7- Convention Plugin Migration in QNB Android Mobile📢

Çağatay Gül
IBTech
Published in
5 min readSep 5, 2024

Herkese Merhaba, ben Çağatay 🖐

QNB Mobil Android uygulamamızda önceki yazılarımızda da belirttiğimiz üzere dependency management anlamında köklü değişikliklere gitmiştik. İlk olarak projemizin gradle dili çokça bilinen Groovy idi. Güncel gelişmeleri takip etmek ve yönetimi kolaylaştırmak için ilk aşama olarak gradle’ı Dsl’e geçirmiştik. Bu geliştirmenin ardından versiyonların ve liblerin ortak yönetimini sağlayabilmek için Version Catalog’a geçiş yaptık. Dependency Management kapsamında ise son aşamamız olan Convention Plugin’i projemizde kullanmaya başladık. Projemiz büyüdükçe bağımlılıkları, gradle dosyalarını yönetmek ve gradle içerisindeki librarylerin, versiyonların bakımını zorlaşmaktaydı. Biz hem bu zorlukları aşmak hem de scriptlerimizi daha okunabilir hale getirmek için plugine geçiş yaptık. Bu makale bir proje de Convention Plugin nasıl yazılır yerine geçiş sırasında karşılaşılan zorluklar, geçişin avantajları, dezavantajlarını ile birlikte tecrübelerimizi sizlere örnekler ile aktarmaya çalıştığımız bir yazı olacak.

Plugin Yazmaya Neden Başladık? 🤔

Bir önceki makalemizde sizlere projemizi multi-moduler yapıya geçirdiğimizden bahsetmiştik. Modularization geçişi sonrasında 17 adet yeni data modülü, 17 adet yeni feature modülü oluşmuş oldu. Bu kapsam sonrasında toplam 34 adet birbirine çok benzer yapıda gradle dosyalarımız oluştu. Belirli bir aşamadan sonra bu dosyaların bakımı ve yönetimi zorlaşmaya başladı çünkü 34 farklı gradle dosyalarında hem data hem de feature modulleri ilgilendiren bir lib ekleme veya silmede 34 ayrı yerde değişiklik yapılması gerekiyordu. Tam bu anda Convention Plugin bizlere için çok avantajlı gelmeye başladı. Çünkü gradle içindeki kodlar birbirine çok benzer yapıdaydı. MBFeatureConventionPlugin ve MBDataConventionPlugin adında 2 adet plugin oluşturduk. Hem data hem de feature modullerinde kullanılan ortak kodları bu plugin içerisinde toplamaya başladık aşağıdaki görsellerde Plugin kullanmadan önceki gradle’ımızın bir kısmını, yazdığımız plugini ve plugin sonrası gradle’ı görebilirsiniz.

Convention Plugin kullanmadan önce Version Catalog ve Dsl ile yazılan data modullerimzden birisine ait gradle dosyamız.👇

plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("com.google.dagger.hilt.android")
id("kotlin-kapt")
}

apply(from = "$rootDir/module_based_detekt.gradle")
apply(from = "$rootDir/common/product_flavor.gradle")
android {
namespace = "com.qnbfinansbank.data.insurance"
compileSdk = libs.versions.compileSdk.get().toInt()
defaultConfig {
minSdk = libs.versions.minSdk.get().toInt()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = libs.versions.jvmTarget.get()
}
}
dependencies {
implementation(project(":core:model"))
implementation(project(":core:data"))
implementation(project(":core:mobilecore-deprecated"))
implementation(libs.kotlin)
implementation(libs.ktxCore)
implementation(libs.retrofit)
implementation(libs.retrofitGson)
// hilt
implementation(libs.hiltAndroid)
kapt(libs.hiltAndroidCompiler)
kapt(libs.hiltCompiler)
}

Data modulleri için yazdığımız Plugin.👇

import com.android.build.api.dsl.LibraryExtension
import common.configureKotlinJvm
import common.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies

class MBDataConventionPlugin: Plugin<Project> {
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
apply("com.android.library")
apply("org.jetbrains.kotlin.android")
apply("kotlin-kapt")
apply("qnbfinansbank.android.hilt")
}
apply(from = "$rootDir/module_based_detekt.gradle")
apply(from = "$rootDir/product_flavor.gradle")
extensions.configure<LibraryExtension> {
compileSdk = libs.findVersion("compileSdk").get().toString().toInt()
defaultConfig {
minSdk = libs.findVersion("minSdk").get().toString().toInt()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
}
configureKotlinJvm()
dependencies {
add("implementation", project(":core:data"))
add("implementation", project(":core:util"))
add("implementation", project(":core:network"))
add("implementation", project(":core:session"))
add("implementation", project(":core:app-util"))
add("implementation", project(":core:database"))
add("implementation", project(":core:constant"))
add("implementation", project(":core:navigation"))
add("implementation", project(":core:application"))
add("implementation", project(":core:network"))
add("implementation", libs.findLibrary("kotlin").get())
add("implementation", libs.findLibrary("ktxCore").get())
add("implementation", libs.findLibrary("retrofit").get())
add("implementation", libs.findLibrary("retrofitGson").get())
add("implementation", libs.findLibrary("timber").get())
}
}
}
}

Yazılan Pluginin ilgili data module implementasyonu sonrası gradle dosyamızın son hali👇

plugins {
alias(libs.plugins.mbDataConventionPlugin)
}

android {
namespace = "com.qnbfinansbank.data.insurance"
}

Sadece Modul gradle’ları için ortak plugin yazılmalıdır gibi düşünmemeliyiz. Hilt, room serialization vs için birden fazla plugin yazılıp pluginler birbirleri içerisinde çağrılabilir. Aşağıda Hilt için yazdığımız plugin örneğini de görebilirsiniz.

import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.dependencies

class MBHiltConventionPlugin: Plugin<Project> {
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
apply("com.google.dagger.hilt.android")
apply("kotlin-kapt")
}
dependencies {
add("implementation", (libs.findLibrary("hiltAndroid").get()))
add("kapt", libs.findLibrary("hiltAndroidCompiler").get())
add("kapt", libs.findLibrary("hiltCompiler").get())
}
}
}
}

Zorluklar & Sorunlar 🛠

Önceki makalelerimizde Groovy’den Dsl’e geçiş sonrası için de Version Catalog’a geçiş adlı iki yazımız bulunmaktaydı ordaki yaşadığımız zorluklar paralel biçimde Convention Plugin için de geçerli diyebiliriz. Bunun en büyük sebebi gradle dosyaları ile uğraşıyor olmak. Pluginleri eklerken karşılaştığımız en genel sorun sync problemleridir. İlgili gradle dosyalarına plugini implemente etmek ve bu süreçte projeyi senkronize etme aşamasında çeşitli hatalar meydana gelir. Bu hatalar genellikle yanlış tanım, eksik bağımlılıklar veya uyumsuzluklardan kaynaklanır. Bu kısımda verebileceğim bir tavsiye plugin yazıldıktan sonra diğer gradle işlemlernde de olduğu gibi aşama aşama ilerlemektir. Örnek olarak data modulleriniz için bir plugin yazacaksanız “DataModuleConventionPlugin” adlı (isimlendirme tamamıyla size kalmış) plugin file oluşturulduktan sonra ilgili data gradlelara eklenerek tek tek sync edilip ilerlenmelidir. Multi-Moduler yapıdaki projeler için bir anda tüm pluginler yazılıp implemente edilip sync edilmemelidir. Pluginin yanlış yapılandırılması, modüller arasında hatalara veya tutarsızlıklara yol açabilir. Örneğin, tüm modüllere uygulanması gereken bir plugin yanlış tanımlanırsa, bu durum gradleların sync sırasında beklenmedik hatalar vermesine sebep olabilir. Gradle içerisindeki kodların plugine taşınması dikkatli planlama ve test gerektirir, aksi takdirde proje üzerinde hatalar ortaya çıkabilir.

Avantaj✔️ & Dezavantaj✖️

Tutarlılık: Convention Plugin’in en büyük faydası, farklı modüller arasında tutarlılığı sağlamasıdır. Android SDK sürümlerinin ayarlanması, kotlin lib version kontroleri veya lint kuralları gibi ortak gradle implementasyonlarının her modülde tekrar edilmesi yerine, bu ayarlar bir kez pluginde tanımlanarak tüm modüllere eşit şekilde uygulanır.

Örnek vermek gerekirse plugin öncesi java versionları her gradlelar içerisinde ayrı ayrı tanımlanmışken şu an plugin için yazdığımız bir extension function ile plugine implemente edilerek tüm gradlelar için tek yerden yönetiliyor.

internal fun Project.configureKotlinJvm() {
extensions.configure<JavaPluginExtension> {
toolchain.languageVersion.set(JavaLanguageVersion.of(libs.findVersion("javaVersion").get().toString()))
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
configureKotlin()
}

Boilerplate Kodun Azaltılması : Ortaklaştırılan pluginlerin gradleda kullanımları boilerplate kod miktarını azaltır. Bu da gradle dosyalarının daha temiz, daha okunabilir ve daha kolay bakım yapılabilir olmasını sağlar.

Bakım Kolaylığı: Proje büyüdükçe, birden fazla modül arasında tutarlılığı sağlamak maliyetimizi arttırmaktadır. Convention Plugin ile ortak gradlelardaki herhangi bir değişiklik tek bir yerde yapılabilir ve bu değişiklikler otomatik olarak tüm modüllere uygulanır. Bu, bakım kolaylığını artırır ve hata olasılığını minimuma indirir.

Aşağıdaki görselde de pluginlerimizi bir nebze sadeleştirebilmek için kullandığımız fonksiyonların bir kısmını görebilirsiniz.

package common

import com.android.build.api.dsl.LibraryExtension
import org.gradle.api.JavaVersion
import org.gradle.api.Project
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.jvm.toolchain.JavaLanguageVersion
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.withType
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
private fun Project.configureKotlin() {
tasks.withType<KotlinCompile>().configureEach {
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
}
}
internal fun Project.configureKotlinJvm() {
extensions.configure<JavaPluginExtension> {
toolchain.languageVersion.set(JavaLanguageVersion.of(libs.findVersion("javaVersion").get().toString()))
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
configureKotlin()
}
internal fun Project.configureBuildFeatures() {
extensions.configure<LibraryExtension> {
viewBinding.enable = true
dataBinding.enable = true
}
}

Uyumluluk: Plugin, bağımlılıkları daha etkili bir şekilde yönetmenize de yardımcı olur. Paylaşılan bağımlılıklar, pluginde bir kez tanımlanarak tüm modüllerin ilgili libleri aynı sürümleri kullanması sağlanır. Bu version uyumsuzluklarının yaşanmasını engeller.

Tek modul ya da küçük projelerde kullanım: Küçük projeler veya az sayıda modüle sahip projeler için, Convention Plugin’in kurulumu ve bakımıyla ilgili ek yük, sağladığı faydaların önüne geçebilir. Bu tür durumlarda, gradleların daha zor anlaşılır ve bakımı bir nebze daha zor hale getirebilir.

Build Süresine Olan Etkisi⌛️⏳

Genel olarak, build süresi üzerindeki etki, projenin büyüklüğüne bağlı olarak değişiklik gösterecektir. Multi-modüler yapıda olan büyük projeler Convention Plugin build sürecini optimize edebilir ve potansiyel olarak build süresini kısaltabilir. Pluginler benzer yapıdaki gradleların sync süresini azaltabilir. Başka bir açıdan küçük projeler için ise Convention Plugin’in tanıttığı ek abstraction katmanı, gradle için build sırasında ek bir yük oluşturabilir ve küçükte olsa build süresinde artış görülebilir.

Sonuç

Convention Plugin, özellikle multi modüllü projelerde tutarlılığı sağlamak ve boilerplate kodu azaltmak için kullanılan etkili bir gradle implementasyon biçimidir. Bakım kolaylığı ve basite indirgenen bağımlılık yönetimi gibi birçok avantaj sunmaktadır. Başlangıçta plugini ayağa kaldırmadaki zorluk ve sync problemlerini göz ardı edersek. Tekrar eden gradleların azaltılmasından ve tek yerden yönetimden kaynaklanan avantajlar genellikle multimodul projelerde daha belirgin olur. Ancak, küçük ve tek modulluk projelerde kullanımının getirdiği ek yükü dikkatlice değerlendirmelidir. Özetle multi-modullü projeleriniz için convention plugin kullanımını tavsiye ediyorken küçük projeler için doğrudan version catalog/dsl yeterli olacaktır.

Okuduğunuz için teşekkürler. Bir sonraki yazımız olan Infrastructure in QNBAndroid Mobile’ da görüşmek üzere.

--

--

Çağatay Gül
IBTech
Writer for

Senior Android Developer at Ibtech, QNBFinansbank