<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
    <channel>
        <title>نوشته های Mahdi Sadeghi</title>
        <link>https://virgool.io/feed/@mahdisadeghiaghdam</link>
        <description>برنامه نویس اندروید موبایلت</description>
        <language>fa</language>
        <pubDate>2026-06-16 12:01:18</pubDate>
        <image>
            <url>https://files.virgool.io/upload/users/62340/avatar/IelR4w.jpg?height=120&amp;width=120</url>
            <title>Mahdi Sadeghi</title>
            <link>https://virgool.io/@mahdisadeghiaghdam</link>
        </image>

                    <item>
                <title>مدریریت وابستگی ها در موبایلت بانک سامان</title>
                <link>https://virgool.io/Rahkar/%D9%85%D8%AF%D8%B1%DB%8C%D8%B1%DB%8C%D8%AA-%D9%88%D8%A7%D8%A8%D8%B3%D8%AA%DA%AF%DB%8C-%D9%87%D8%A7-%D8%AF%D8%B1-%D9%85%D9%88%D8%A8%D8%A7%DB%8C%D9%84%D8%AA-%D8%A8%D8%A7%D9%86%DA%A9-%D8%B3%D8%A7%D9%85%D8%A7%D9%86-l0lt4wy80yz6</link>
                <description>سلام به همه برنامه‌نویسان اندروید عزیز! 👋این اولین نوشته از مجموعه مقالاتیه که ما در تیم شرکت راهکار، تصمیم گرفتیم تجربه‌ها و دستاوردهای فنی‌مون رو با شما به اشتراک بذاریم. ما توی راهکار، همیشه به دنبال راه‌هایی برای بهبود فرآیند توسعه و ارتقاء کیفیت پروژه‌هامون هستیم. هدفمون از این مقالات، ایجاد یک فضای دوستانه برای یادگیری و تبادل نظر بین برنامه‌نویسان اندروید ایرانه.لازم به ذکر هست که این مقاله، حاصل تلاش‌های ارزشمند تیم موبایل راهکار هست، و ما صمیمانه از نقش کلیدی همکار عزیزمون، آقای امیرحسین اسدپور، در توسعه و پیاده‌سازی این راهکار قدردانی می‌کنیم. این پلاگین‌ها توسط امیرحسین اسدپور توسعه داده شده است.توی این مقاله، می‌خوایم در مورد یکی از چالش‌های اساسی که توی پروژه‌های بزرگ و ماژولار اندروید باهاش دست و پنجه نرم کردیم صحبت کنیم: مدیریت وابستگی‌ها (Dependency Management). اگه شما هم تجربه کار با پروژه‌های چند ماژولی رو داشته باشید، حتماً می‌دونید که تکرار وابستگی‌ها توی ماژول‌های مختلف و انجام تنظیمات مشابه برای هر ماژول، چقدر می‌تونه دردسرساز باشه.تیم اندروید موبایلت برای حل این مشکل و داشتن یک ساختار منظم‌تر و کارآمدتر، یک راه حل خلاقانه رو پیاده‌سازی کرد: ساخت و استفاده از کانونشن پلاگین‌های Gradle اختصاصی. توی این مقاله، بهتون می‌گیم که چرا این تصمیم رو گرفتیم، چه مزایایی داشت، و چطور چند تا از پلاگین‌های کلیدی‌مون رو ساختیم.چرا مدیریت وابستگی‌ها توی پروژه‌های ماژولار مهمه؟قبل از اینکه بریم سراغ اصل مطلب و پلاگین‌هامون رو معرفی کنیم، بذارید یکم در مورد اهمیت مدیریت درست وابستگی‌ها توی پروژه‌های ماژولار صحبت کنیم. وقتی پروژه اندرویدتون از چند تا ماژول تشکیل شده (چه ماژول‌های Feature، چه Library و چه Dynamic Feature)، مدیریت وابستگی‌ها کم کم تبدیل میشه به یه چالش بزرگ.تصور کنید که یه سری کتابخونه پرکاربرد مثل androidx.core-ktx, appcompat, material یا کتابخونه‌های تست رو توی اکثر ماژول‌هاتون استفاده می‌کنید. اگه یه سیستم مدیریت متمرکز نداشته باشید، مجبورید این وابستگی‌ها رو به صورت جداگانه توی فایل build.gradle هر ماژول تعریف کنید. این کار کلی مشکل به وجود میاره:تکرار کد: هی یه وابستگی رو توی چند تا فایل build.gradle تعریف می‌کنید و این باعث میشه کدتون زیاد بشه و احتمال خطا هم بالا بره.ناسازگاری نسخه‌ها: ممکنه یهو یادتون بره یه کتابخونه رو توی یه ماژول آپدیت کنید و این باعث میشه نسخه‌های کتابخونه‌ها توی ماژول‌های مختلف با هم فرق داشته باشن و کلی مشکل عجیب و غریب موقع اجرا به وجود بیاد.شلوغ شدن فایل‌های build.gradle: فایل‌های build.gradleتون خیلی شلوغ میشن و دیگه خوندنشون سخت میشه.سختی به‌روزرسانی‌ها: اگه بخواید یه کتابخونه رو توی کل پروژه آپدیت کنید، باید کلی فایل build.gradle رو تغییر بدید که هم زمان‌بره و هم ممکنه یه جا یادتون بره و خرابکاری بشه.عدم انسجام توی تنظیمات: علاوه بر وابستگی‌ها، یه سری تنظیمات دیگه هم مثل compileSdk, minSdk, تنظیمات کامپایلر Kotlin و فعال کردن ViewBinding و Compose هستن که ممکنه توی ماژول‌های مختلف تکرار بشن.تاریخچه مدیریت وابستگی‌ها در Gradle قبل از Convention Pluginsقبل از ظهور Convention Plugins و Version Catalogs، توسعه‌دهندگان اندروید و Gradle از روش‌های مختلفی برای مدیریت وابستگی‌ها و تنظیمات استفاده می‌کردند که هر کدام چالش‌های خاص خود را داشتند.روش‌های پیشین:۱. تعریف مستقیم در هر build.gradle:ابتدایی‌ترین روش، تعریف مستقیم هر وابستگی در فایل build.gradle مربوط به هر ماژول بود. این روش برای پروژه‌های کوچک با چند ماژول قابل مدیریت بود، اما همانطور که در بالا اشاره شد، با بزرگ شدن پروژه و افزایش ماژول‌ها، به سرعت به کابوسی از کد تکراری، ناسازگاری نسخه‌ها و مشکلات به‌روزرسانی تبدیل می‌شد.مثال کد (قبل):// app/build.gradle.kts
plugins {
    id(&quot;com.android.application&quot;)
    kotlin(&quot;android&quot;)
    kotlin(&quot;kapt&quot;)
}

android {
    compileSdk = 34
    defaultConfig {
        minSdk = 21
        targetSdk = 34
        // ...
    }
    buildFeatures {
        viewBinding = true
    }
    // ...
}

dependencies {
    implementation(&quot;androidx.core:core-ktx:1.10.0&quot;)
    implementation(&quot;androidx.appcompat:appcompat:1.6.1&quot;)
    implementation(&quot;com.google.android.material:material:1.9.0&quot;)
    implementation(&quot;com.google.dagger:hilt-android:2.44&quot;)
    kapt(&quot;com.google.dagger:hilt-compiler:2.44&quot;)
    // ...
}

// library_feature/build.gradle.kts
plugins {
    id(&quot;com.android.library&quot;)
    kotlin(&quot;android&quot;)
    kotlin(&quot;kapt&quot;)
}

android {
    compileSdk = 34
    defaultConfig {
        minSdk = 21
        // ...
    }
    buildFeatures {
        viewBinding = true
    }
    // ...
}

dependencies {
    implementation(&quot;androidx.core:core-ktx:1.10.0&quot;)
    implementation(&quot;com.google.dagger:hilt-android:2.44&quot;)
    kapt(&quot;com.google.dagger:hilt-compiler:2.44&quot;)
    // ...
}۲. استفاده از بلاکext در build.gradle روت:یکی از اولین تلاش‌ها برای متمرکز کردن نسخه‌ها، استفاده از بلاک ext در فایل build.gradle روت پروژه بود. در این روش، نسخه‌ها به عنوان متغیر تعریف می‌شدند و سپس در ماژول‌های مختلف به آن‌ها ارجاع داده می‌شد. این کار تا حدی مشکل ناسازگاری نسخه‌ها را حل می‌کرد، اما هنوز هم نیاز به تعریف وابستگی‌های کامل در هر ماژول وجود داشت.// build.gradle (Project level)
buildscript {
    ext {
        kotlin_version = &quot;1.9.0&quot;
        hilt_version = &quot;2.44&quot;
        // ...
    }
    // ...
}

// app/build.gradle
dependencies {
    implementation(&quot;com.google.dagger:hilt-android:$hilt_version&quot;)
    kapt(&quot;com.google.dagger:hilt-compiler:$hilt_version&quot;)
}۳. استفاده از برای تعریف Constantها:با پیشرفت Gradle و نیاز به منطق پیچیده‌تر، بسیاری از پروژه‌ها شروع به استفاده از buildSrc کردند. این پوشه یک پروژه Gradle مستقل است که قبل از بقیه پروژه کامپایل می‌شود و به شما اجازه می‌دهد تا کد Kotlin یا Groovy بنویسید و Constantها و توابع کمکی را تعریف کنید. این روش به شما اجازه می‌داد تا لیست کامل وابستگی‌ها را به عنوان object یا class تعریف کنید و آن‌ها را در build.gradle ماژول‌ها استفاده کنید. این یک گام بزرگ رو به جلو بود، اما هنوز هم برای تنظیمات پیچیده یا اعمال پلاگین‌های متعدد نیاز به کپی‌پیست داشت.معرفی Convention Plugins و Version Catalogsاین Convention Plugins به عنوان راه حلی قدرتمند برای این مشکلات معرفی شدند. به طور دقیق Gradle در نسخه 6.0 (در سال 2019) مفهوم precompiled script plugins را معرفی کرد که اساس Convention Plugins امروزی است. هدف اصلی این قابلیت، حذف تکرار در فایل‌های build.gradle و متمرکز کردن منطق پیکربندی بود.با معرفی precompiled script plugins (که بعداً به Convention Plugins شهرت یافتند)، توسعه‌دهندگان می‌توانستند بخش‌های تکراری build.gradle را به پلاگین‌های مجزا تبدیل کنند. این پلاگین‌ها می‌توانستند هم وابستگی‌ها و هم تنظیمات Gradle را به صورت یکپارچه اعمال کنند.همچنین، با معرفی Version Catalogs در Gradle 7.0 (در سال 2021)، مدیریت نسخه‌های وابستگی‌ها حتی ساده‌تر و متمرکزتر شد. فایل libs.versions.toml که پیش‌تر به آن اشاره شد، بخشی از این سیستم است که به شما امکان می‌دهد تمام وابستگی‌ها، پلاگین‌ها و نسخه‌ها را در یک مکان واحد تعریف کنید.هدف اصلی Convention Plugins تنها مدیریت وابستگی‌ها نیست، بلکه متمرکز کردن هرگونه پیکربندی و منطق Gradle است که در چندین ماژول تکرار می‌شود. این شامل تنظیمات SDK، گزینه‌های کامپایلر، فعال‌سازی فیچرها (مانند ViewBinding یا Compose)، و حتی اعمال پلاگین‌های دیگر (مثل Hilt یا Lint) می‌شود. این ویژگی آن‌ها را به ابزاری قدرتمند برای ایجاد یکپارچگی و کاهش boilerplate code در پروژه‌های بزرگ و ماژولار تبدیل می‌کند.راهکار تیم موبایلت: پلاگین‌های Gradle اختصاصیبرای اینکه از شر این مشکلات خلاص بشیم، تیم اندروید موبایلت تصمیم گرفت یه راه حل اساسی برای خودمون پیدا کنیم. ما از قدرت Gradle استفاده کردیم و پلاگین‌های اختصاصی خودمون رو ساختیم. لازم به ذکره که ایده و الهام اصلی این پیاده‌سازی، از پروژه متن‌باز Now in Android گوگل گرفته شده . این پروژه، یک نمونه‌ عالی از پیاده‌سازی Convention Plugins رو داره. و ما با بررسی آن، این راهکار را برای نیازهای پروژه‌های خودمان پیاده سازی کردیمبا این کار تونستیم:وابستگی‌های مشترک رو به صورت متمرکز مدیریت کنیم: همه کتابخونه‌هایی که توی بیشتر ماژول‌هامون استفاده میشن رو یه جا تعریف کردیم و به راحتی توی ماژول‌های مختلف ازشون استفاده کردیم.یه سری تنظیمات پیش‌فرض و مشترک رو اعمال کنیم: تنظیماتی مثل نسخه‌های compileSdk و minSdk, تنظیمات کامپایلر Kotlin، و فعال کردن ViewBinding و Compose رو به صورت خودکار روی ماژول‌هامون اعمال کردیم.حجم فایل‌های build.gradle رو کم کنیم و خوانایی‌شون رو بالا ببریم: با استفاده از پلاگین‌ها، فایل‌های build.gradle ماژول‌هامون خیلی خلوت‌تر و تمیزتر شدن.به‌روزرسانی‌ها رو راحت‌تر کنیم: برای به‌روزرسانی یه کتابخونه یا تغییر یه تنظیم، فقط کافیه پلاگین‌ها رو تغییر بدیم و این تغییرات به صورت خودکار روی همه ماژول‌های مربوطه اعمال میشه.سرعت توسعه رو افزایش بدیم و خطاها رو کم کنیم: با حذف کارهای تکراری و داشتن تنظیمات یکسان، سرعت توسعه‌مون خیلی بیشتر شده و احتمال خطاها هم خیلی کمتر شده.نگاهی به پلاگین‌های ما:حالا بیاید چند تا از پلاگین‌های Gradle اختصاصی که توی تیم موبایلت ساختیم رو با هم ببینیم و نحوه کارکرد و مزایاشون رو بررسی کنیم. کدهایی که توی توضیحات قبلی به اشتراک گذاشتید، نمونه‌هایی از این پلاگین‌های قدرتمند هستن.1. AndroidApplicationConventionPlugin:این پلاگین برای پیکربندی ماژول‌های Application (همون ماژول اصلی اپلیکیشن) استفاده میشه. همونطور که توی کد می‌بینید:class AndroidApplicationConventionPlugin : Plugin&lt;Project&gt; {
    override fun apply(target: Project) {
        with(target) {
            with(pluginManager) {
                apply(libs.findPlugin(&quot;android-application&quot;).get().get().pluginId)
                apply(&quot;kotlin-android&quot;)
                apply(&quot;kotlin-kapt&quot;)
            }
            extensions.configure&lt;ApplicationExtension&gt; {
                configureKotlinAndroid(this)
                defaultConfig.targetSdk = 34
            }
        }
    }
}این پلاگین اول پلاگین‌های com.android.application، kotlin-android و kotlin-kapt رو اعمال می‌کنه. این کار باعث میشه که قابلیت‌های اصلی توسعه اپلیکیشن اندروید و پشتیبانی از کاتلین و KAPT به ماژول اضافه بشه. بعدش از طریق extensions.configure&lt;ApplicationExtension&gt;، تنظیمات مربوط به ApplicationExtension رو پیکربندی می‌کنه. اینجا، تابع configureKotlinAndroid (که توی فایل ir.mobillet.app تعریف شده) برای اعمال تنظیمات مشترک کاتلین و تنظیم targetSdk به 34 فراخوانی میشه.2. AndroidLibraryConventionPlugin:این پلاگین برای پیکربندی ماژول‌های Library (کتابخانه‌های اندروید) استفاده میشه:class AndroidLibraryConventionPlugin : Plugin&lt;Project&gt; {
    override fun apply(target: Project) {
        with(target) {
            with(pluginManager) {
                listOf(
                    &quot;androidLibrary&quot;,
                    &quot;kotlin-android-gradle&quot;
                )
                    .map { libs.findPlugin(it).get().get().pluginId }
                    .forEach { apply(it) }
                apply(&quot;mobillet.android.flavors&quot;)
                apply(&quot;kotlin-android&quot;)
                apply(&quot;kotlin-kapt&quot;)
                apply(&quot;kotlin-parcelize&quot;)
                apply(&quot;mobillet.android.lint&quot;)
                apply(&quot;mobillet.android.hilt&quot;)
            }
            extensions.configure&lt;LibraryExtension&gt; {
                configureKotlinAndroid(this)
                defaultConfig {
                    vectorDrawables.useSupportLibrary = true
                }
                @Suppress(&quot;UnstableApiUsage&quot;)
                testOptions {
                    unitTests.isReturnDefaultValues = true
                }
                buildFeatures {
                    viewBinding = true
                }
                buildTypes {
                    release {
                        isMinifyEnabled = true
                        proguardFiles(
                            getDefaultProguardFile(&quot;proguard-android-optimize.txt&quot;),
                            &quot;proguard-rules.pro&quot;
                        )
                    }
                }
            }
        }
    }
}این پلاگین یه عالمه پلاگین مورد نیاز برای یه ماژول Library رو اعمال می‌کنه، از جمله com.android.library، kotlin-android-gradle، پلاگین‌های مربوط به Flavorها (mobillet.android.flavors که اینجا جزییاتش نیست)، kotlin-android، kotlin-kapt، kotlin-parcelize، Lint (mobillet.android.lint) و Hilt (mobillet.android.hilt). بعدش تنظیمات مربوط به LibraryExtension رو پیکربندی می‌کنه:فراخوانی configureKotlinAndroid برای تنظیمات مشترک کاتلین.فعال کردن vectorDrawables.useSupportLibrary.تنظیم unitTests.isReturnDefaultValues برای تست‌های واحد.فعال کردن viewBinding.پیکربندی تنظیمات مربوط به buildTypes به خصوص برای Build Type release (فعال کردن Minify و تنظیم فایل‌های Proguard).3. AndroidHiltConventionPlugin:این پلاگین برای مدیریت وابستگی‌های مربوط به Hilt (Dependency Injection) توی ماژول‌ها استفاده میشه:class AndroidHiltConventionPlugin : Plugin&lt;Project&gt; {
    override fun apply(target: Project) {
        with(target) {
            with(pluginManager) {
                apply(libs.findPlugin(&quot;hilt-android&quot;).get().get().pluginId)
            }
            dependencies {
                &quot;implementation&quot;(libs.findLibrary(&quot;hilt-androidPart&quot;).get())
                &quot;kapt&quot;(libs.findLibrary(&quot;hilt-compiler&quot;).get())
            }
        }
    }
}این پلاگین به راحتی پلاگین dagger.hilt.android.plugin رو اعمال می‌کنه و بعدش وابستگی‌های مورد نیاز برای Hilt (hilt-android و hilt-compiler) رو با استفاده از libs.findLibrary() (که به libs.versions.toml اشاره داره) به ماژول اضافه می‌کنه. توجه کنید که hilt-compiler توی scope kapt اضافه شده.4. AndroidApplicationComposeConventionPlugin:این پلاگین برای پیکربندی ماژول‌هایی که از Jetpack Compose استفاده می‌کنند طراحی شده:class AndroidApplicationComposeConventionPlugin : Plugin&lt;Project&gt; {
    override fun apply(target: Project) {
        with(target) {
            try {
                extensions.configure&lt;LibraryExtension&gt; {
                    configureAndroidCompose(this)
                }
            } catch (e: UnknownDomainObjectException) {
                extensions.configure&lt;ApplicationExtension&gt; {
                    configureAndroidCompose(this)
                }
            }
        }
    }
}این پلاگین تلاش می‌کنه تا configureAndroidCompose رو روی LibraryExtension اعمال کنه و اگه اون وجود نداشت (که نشون میده ماژول Application هست)، اون رو روی ApplicationExtension اعمال می‌کنه. این نشون میده که تنظیمات Compose هم توی ماژول‌های Feature (که معمولاً Library هستن) و هم توی ماژول اصلی Application قابل استفاده هستن. تابع configureAndroidCompose (که توی فایل ir.mobillet.app تعریف شده) تنظیمات مربوط به Compose رو انجام میده.البته توی این مورد میتونیم به جای استفاده از try-catch، توی پلاگین جدا، یکی برای Application و دیگری برای Library بسازیم و هر کدوم رو در جای مورد نیازش استفاده کنیم.5. AndroidLintConventionPlugin:این پلاگین برای اعمال تنظیمات مربوط به Lint (ابزار آنالیز کد استاتیک) استفاده میشه:class AndroidLintConventionPlugin : Plugin&lt;Project&gt; {
    override fun apply(target: Project) {
        with(target) {
            with(pluginManager) {
                apply(libs.findPlugin(&quot;ktlint&quot;).get().get().pluginId)
            }
        }
    }
}این پلاگین به سادگی پلاگین ktlint رو اعمال می‌کنه، که به تیم اجازه میده استایل کد کاتلین رو توی کل پروژه به صورت یکنواخت حفظ کنه.مدیریت نسخه‌ها با libs.versions.toml:همونطور که توی کد پلاگین AndroidHiltConventionPlugin دیدید، ما از libs.findLibrary() برای دسترسی به وابستگی‌ها استفاده می‌کنیم. این متد از یه فایل به اسم libs.versions.toml میاد که توی ساختار build-logic پروژه تعریف شده. این فایل یه روش متمرکز و سازمان‌یافته برای مدیریت نسخه‌های کتابخونه‌ها و پلاگین‌ها توی کل پروژه فراهم می‌کنه.فایل dependencyResolutionManagement توی فایل settings.gradle.kts پروژه build-logic نحوه تعریف این کاتالوگ نسخه رو نشون میده:dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
    }
    versionCatalogs {
        create(&quot;libs&quot;) {
            from(files(&quot;../gradle/libs.versions.toml&quot;))
        }
    }
}و فایل plugins توی build.gradle.kts پروژه build-logic نحوه تعریف Alias برای پلاگین‌ها رو نشون میده:plugins {
    alias(libs.plugins.android.application) apply false
// ... سایر پلاگین ها
}با استفاده از این سیستم، به راحتی میشه نسخه‌های کتابخونه‌ها و پلاگین‌ها رو یه جا به‌روزرسانی کرد و از ناسازگاری نسخه‌ها توی ماژول‌های مختلف جلوگیری کرد.چطوری از پلاگین‌ها توی ماژول‌هامون استفاده می‌کنیم؟برای استفاده از این پلاگین‌ها توی ماژول‌های مختلف پروژه اندروید، کافیه اون‌ها رو توی بخش plugins فایل build.gradle ماژول مورد نظر اعمال کنیم.مثال کد (بعد از استفاده از Convention Plugins):// app/build.gradle.kts
plugins {
    id(&quot;mobillet.android.application&quot;)
    id(&quot;mobillet.android.hilt&quot;)
    id(&quot;mobillet.android.compose&quot;) 
    id(&quot;mobillet.android.lint&quot;) 
}

android {
    // دیگه نیازی به تنظیمات تکراری compileSdk, minSdk, targetSdk, viewBinding, etc. نیست!
    // همه این‌ها در Convention Plugin مدیریت شده‌اند.
}

dependencies {
    // فقط وابستگی‌های خاص این ماژول که عمومی نیستند
    implementation(project(&quot;:feature:some_feature&quot;)) // وابستگی به ماژول فیچر
}

// library_feature/build.gradle.kts
plugins {
    id(&quot;mobillet.android.library&quot;) 
    id(&quot;mobillet.android.hilt&quot;) 
    id(&quot;mobillet.android.compose&quot;) 
    id(&quot;mobillet.android.lint&quot;)  // برای استفاده از Lint
}

android {
    // دیگه نیازی به تنظیمات تکراری نیست!
}

dependencies {
    // فقط وابستگی‌های خاص این ماژول
    implementation(libs.androidx.datastore.preferences) // مثال: یک وابستگی خاص این ماژول
}همونطور که می‌بینید، فایل build.gradle ماژول‌ها خیلی تمیزتر و متمرکز بر وابستگی‌ها و تنظیمات خاص خود ماژول شده.مزایای کلی استفاده از پلاگین‌های Gradle اختصاصی:افزایش سرعت بیلد: با کم شدن تنظیمات تکراری، Gradle می‌تونه سریع‌تر بیلد کنه.بهبود سازماندهی کد: فایل‌های build.gradle تمیزتر و خواناتر میشن و تمرکز روی منطق ماژول بیشتر میشه.افزایش قابلیت استفاده مجدد از کد: تنظیمات و وابستگی‌های مشترک به صورت یکپارچه مدیریت میشن.کاهش خطاهای انسانی: با حذف کارهای تکراری، احتمال خطا توی تنظیمات کم میشه.بهبود تجربه توسعه‌دهنده: برنامه‌نویس‌ها می‌تونن بیشتر روی کد تمرکز کنن و کمتر درگیر تنظیمات Gradle بشن.انسجام توی کل پروژه: خیالمون راحته که همه ماژول‌ها از نسخه‌های یکسان کتابخونه‌ها و تنظیمات مشابه استفاده می‌کنن.نتیجه‌گیری: سرمایه‌گذاری برای آینده‌ای بهترتصمیم تیم موبایلت برای ساخت پلاگین‌های Gradle اختصاصی، یه سرمایه‌گذاری ارزشمند برای بهبود کیفیت و کارایی فرآیند توسعه پروژه‌های اندرویدمون بوده. این رویکرد نه تنها مشکلات مدیریت وابستگی‌ها و پیکربندی‌های تکراری رو حل کرده، بلکه باعث افزایش سرعت توسعه، بهبود سازماندهی کد و کاهش خطاهای انسانی شده.امیدواریم این مقاله برای شما برنامه‌نویسان اندروید هم مفید بوده باشه و بهتون نشون بده که چطور میشه با استفاده از Gradle، مشکلات پیچیده رو به یه راه حل ساده و کارآمد تبدیل کرد. ما توی تیم موبایلت، همیشه مشتاق یادگیری و به اشتراک گذاشتن تجربه‌هامون هستیم. اگه سوالی دارید یا دوست دارید بیشتر در مورد این موضوع صحبت کنیم، حتماً توی بخش نظرات با ما در ارتباط باشید.در مقالات بعدی، به سراغ موضوعات جذاب دیگه‌ای از دنیای توسعه اندروید خواهیم رفت!</description>
                <category>Mahdi Sadeghi</category>
                <author>Mahdi Sadeghi</author>
                <pubDate>Mon, 09 Jun 2025 14:54:02 +0330</pubDate>
            </item>
                    <item>
                <title>نکاتِ خیلی مهمِ کدنویسیِ تمیز در اندروید</title>
                <link>https://virgool.io/coderlife/%D9%86%DA%A9%D8%A7%D8%AA%D9%90-%D9%85%D9%87%D9%85%D9%90-%DA%A9%D8%AF%D9%86%D9%88%DB%8C%D8%B3%DB%8C%D9%90-%D8%AA%D9%85%DB%8C%D8%B2-%D8%AF%D8%B1-%D8%A7%D9%86%D8%AF%D8%B1%D9%88%DB%8C%D8%AF-alqonj0iis87</link>
                <description>در برنامه نویسی اندروید یکی از نکات مهمی که نمیتونیم ازش غافل بشیم کدنویسی تمیزه. و بدون کد تمیز امکان نگهداری و توسعه اپلیکیشن سخت و حتی میتونه غیر ممکن باشه! برای همین یک سری نکات برای رعایت این اصول وجود داره که توی این مقاله بهش اشاره میکنیم (اینجا نمیخوام درباره کلین کد و اینا حرف بزنم. قضیه ساده تر و مهم تر از این حرفاست که اغلب رعایت نمیشه):1.Project guidelines1.1 Project structure پروژه ها باید از ساختار دستورالعمل های پروژه پیروی کنند که در این لینک (Android Gradle plugin user guide) تعریف شده. پروژه ribot Boilerplate یه منبع خوب برای یادگیری این مورده.1.2 Project structure 1.2.1 Class filesکلاس ها باید با قوانین UpperCamelCase نام گذاری بشوند.کلاس هایی که از کلاس کامپوننت اندروید ارث بری شدن باید به اسم همون کامپوننت ختم بشن. به عنوان مثال:SignInActivity, SignInFragment,ImageUploaderService,ChangePasswordDialog.1.2.2 Resources filesریسورس ها باید از قوانین lowercase_underscore پیروی کنند.1.2.2.1 Drawable filesقرارداد اسم گذاری برای برای drawable ها:قرارداد اسم گذاری برای برای icon ها(Android iconography guidelines):قرارداد اسم گذاری برای برای selector state ها:1.2.2.2 Layout filesلایوت ها باید با کامپوننت هاشون مچ بشن. مانند تصویر زیر:1.2.2.3 Menu filesمشابه بالا نام منو ها باید با نام کامپوننتش  مطابقت داشته باشه. به عنوان مثال اگه ما یک menu تعریف کنیم که توی UserActivity استفاده میشه پس اسمش باید activity_user.xml باشه.اینکه کلمه menu بخشی از اسم منو باشه روش درستی نیست چون که خودش داخل menu directory قرار داره پس مشخص شده که این فایلِ xml یک منو هست.1.2.2.4 Values filesریسورس فایل هایی که باید توی پوشه values قرار داشته باشنم. مانند:strings.xml, styles.xml, colors.xml, dimens.xml, attrs.xml2.Code guidelines2.1 Java language rules2.1.1 Don&#x27;t ignore exceptionsهرگز استثناها رو به حال خودشون رها نکنید:void setServerPort(String value) {
    try {
        serverPort = Integer.parseInt(value);
    } catch (NumberFormatException e) { }
}همونطور که می بینید کد بالا به دلیل این که پیش بینی میکنیم هیچوقت وارد قسمت catch نمیشه اون رو خالی گذاشتیم! بهتره هیچوقت اینکار رو نکینم! راه درست و اصولی اینه که همه exception ها رو هندل کنیم و اونا رو به حال خودشون رها نکنیم.2.1.2 Don&#x27;t catch generic exceptionکاری که تو کد پایین انجام شده اشتباهه:try {
    someComplicatedIOFunction();        // may throw IOException
    someComplicatedParsingFunction();   // may throw ParsingException
    someComplicatedSecurityFunction();  // may throw SecurityException
    // phew, made it all the way
} catch (Exception e) {                 // I&#039;ll just catch all exceptions
    handleError();                      // with one generic handler!
}این کار اصولی نیست که استثناها رو به شکل عمومی هندل کنیم. چون اینکار باعث میشه که exception هایی که هرگز انتظارش رو نداریم(شامل استثناهای زمان اجرا مثل: ClassCastException) در سطح اپلیکیشن اتفاق بیوفته. این به این معنیه که اگه کسی استثنایی اضافه کنه چون ما بهش گفتیم که در صورت بروز هر نوع خطایی به exception عمومی بره پس کامپایلر اون استثنارو هندل نمیکنه!برای فهم بهتره اینکه چرا نباید اینکار رو بکنیم میتونید به این لینک برید.2.1.3 Don&#x27;t use finalizersدر اینجا finalizer هنگام اجرای زباله روبی (یا همون garbage collected خودمون) اجرای میشه در حالی که اینکار میتونه برای پاک کردن منابع اللخصوص منابع خارجی مفید باشه اما مشخص نیست اون چه زمانی صدا زده میشه(یا حتی اصلا صدا زده میشه یا نه؟!)اندرویید از finalizer ها استفاده نمیکنه. پس بجاش میتونیم با هندل کردن درست exception ها اینکار رو انجام بدیم. اما اگه واقعا نیاز به استفاده ازش داشتیم میتونیم یک تابع مانند ()close بنویسیم و با نوشتن یک دستورشرطی هر موقع که نیاز بود ازش استفاده کنیم.2.1.4 Fully qualify importsزمانی که میخوایم از کلاسی مثل Bar در پکیج foo استفاده کنیم دو راه داریم.1- راه بد: import foo.*;2-راه خوب:import foo.Bar;اگه به همه کلاس های foo نیاز نداریم پس بهتره فقط اونی که نیاز داریم رو import کنیم.2.2 Java style rules2.2.1 Fields definition and namingفیلد ها باید بالای فایل تعریف بشن و از قوانین اسم گذاری زیر پیروی کنند:اسم فیلد های پرایویت و غیر استاتیک باید با حرف m شروع بشن.اسم فیلد های پرایویت و استاتیک باید با حرف s شروع بشن.اسم بقیه فیلدها باید با حرف کوچیک شروع بشن.همه حروف فیلد های فاینال و استاتیک باید با حروف بزرگ نوشته بشه و با آندرلاین جدا بشه.مثال:public class MyClass {
    public static final int SOME_CONSTANT = 42;
    public int publicField;
    private static MyClass sSingleton;
    int mPackagePrivate;
    private int mPrivate;
    protected int mProtected;
}2.2.3 Treat acronyms as wordsبا کلمات اختصاری مثل حروف رفتار کنید. مثلا آیدی رو باید به شکل Id بنویسیم نه ID. مانند عکس زیر:2.2.4 Use spaces for indentationاز فضاها برای تو رفتگی ها استفاده کنید.4 فاصله برای بلاک هاif (x == 1) {
    x++;
}8 فاصله برای line wrap ها(فارسیش نمیدونم چی میشه:( )Instrument i =
        someLongthat, wouldNotFit, on, one, line);2.2.5 Use standard brace styleبراکت ها رو درست توی همون خط بزارید نه یک بعدش.class MyClass {
    int func() {
        if (something) {
            // ...
        } else if (somethingElse) {
            // ...
        } else {
            // ...
        }
    }
}استفاده از براکت ها ضروریه درصورتی که کد ما بیشتر از یک خط باشه.اگه کد از ماکسیمم طول خط کمتره استفاده از براکت ها ضروری نیست.کد خوب:if (condition) body();کد بد:if (condition)
    body();  // bad!2.2.6 Annotations2.2.6.1 Annotations practicesبرخی از annotation ها با توجه به استاندارد اندروید عبارت اند از:- هر زمان که میخوایم متدی در کلاس والد رو override کنیم باید از Override@ در بالای متدمون استفاده کنیم.- ممکنه گاهی متدما یک warning داشته باشه اما ما اطمینان داریم که هشدار برطرفه میشه در این مواقع میتونیم از SuppressWarnings@ استفاده میکنیم. این کار باعث میشه کامپایلر هشدار مربوط به اون متد رو نادیده بگیره.برای مطالعه بیشتر میتونید به این لینک مراجعه کنید.2.2.6.2 Annotations styleکلاس ها، متدها، سازنده ها:زمانی که annotation ها به یک کلاس، متد یا سازنده اضافه میشوند باید هر کدام در یک خط جداگانه نوشته بشوند./* This is the documentation block about the class */
@AnnotationA
@AnnotationB
public class MyAnnotatedClass { }فیلدها:زمانی که annotation ها به یک فیلد اضافه میشن باید در یک خط نوشته بشن مگر اینکه طول اونها از ماکسیمم طول خط بیشتر باشه.@Nullable @Mock DataManager mDataManager;2.2.7 Limit variable scopeدامنه متغیرهای محلی باید حداقل باشه. با انجام اینکار ما خوانایی و قابلیت نگهداری کدمون رو افزایش و احتمال بروز خطا رو کاهش میدیم. هر متغیر باید در محدوده بلاکی تعریف بشه که اون متغیر فقط در اون محدوده تاثیر گذاره.متغیر های محلی باید زمانی تعریف بشن که برای اولین بار استفاده میشن. همه متغیر های محلی زمانی که تعریف میشن باید دارای مقدار اولیه باشن. اگه اطلاع کافی برای مقداردهی اولیه یک متغیر محلی نداریم باید تا زمان تعریف کردن اون دست نگهداریم.2.2.8 Order import statementsترتیب import ها: ترتیب import ها رو خود اندروید استدیو رعایت میکنه پس لازم نیست نگران این مورد باشید اما دونستنش خالی از لطف نیست.ترتیب به این صورته:1 - import  های اندروید2 - import های third parties(com, junit, net, org)3 - import های java و javax4 - import های همون پروژه2.2.9 Logging guidelinesاستفاده از متد های لاگ برای شناسایی مشکلات در اختیار برنامه نویس ها قرار میگیره.Log.v(String tag, String msg) (verbose)Log.d(String tag, String msg) (debug)Log.i(String tag, String msg) (information)Log.w(String tag, String msg) (warning)Log.e(String tag, String msg) (error)یک قانون کلی وجود داره که ما از اسم کلاس ها به عنوان tag استفاده میکنیم و اون رو بالای یک کلاس به شکل static final تعریف میکنیم.public class MyClass {
    private static final String TAG = MyClass.class.getSimpleName();

    public myMethod() {
        Log.e(TAG, &amp;quotMy error message&amp;quot);
    }
}لاگ های VERBOSE و DEBUG باید هنگام ساخت نسخه نهایی غیر فعال بشن. حتی خوبه که بقیه لاگ ها هم غیرفعال بشن اما ممکنه که تشخیص بدید که استفاده از لاگ ها ممکنه برای کشف مشکلات مفید باشه که در اون صورت باید مطمئن بیشید که اطلاعاتی مثل آدرس ایمیل، آیدی و... نشت نکنه!برای اینکه لاگ هارو در نسخه دیباگ نشون بدیم:if (BuildConfig.DEBUG) Log.d(TAG, &amp;quotThe value of x is &amp;quot + x);2.2.10 Class member orderingهیچ راه حل درستی برای ترتیب اعضای کلاس وجود نداره اما میتونیم از یک روش منطقی و ثابت برای خوانایی کدمون استفاده کنیم.ترتیبی که توصیه میشه به شکل زیره:ConstantsFieldsConstructorsOverride methods and callbacks (public or private)Public methodsPrivate methodsInner classes or interfacesبه عنوان مثال:public class MainActivity extends Activity {
    private static final String TAG = MainActivity.class.getSimpleName();

    private String mTitle;
    private TextView mTextViewTitle;

    @Override
    public void onCreate() {
        ...
    }

    public void setTitle(String title) {
    	mTitle = title;
    }

    private void setUpView() {
        ...
    }

    static class AnInnerClass {

    }

}اگه کلاسمون از کامپوننت های اندروید مثل اکتیویتی و فرگمنت ارث بری میکنه و متد های لایف سایکل رو اورراید میکنیم ترتیب درست به شکل زیره:public class MainActivity extends Activity {

	//Order matches Activity lifecycle
    @Override
    public void onCreate() {}

    @Override
    public void () {}

    @Override
    public void () {}

    @Override
    public void onDestroy() {}

}2.2.11 Parameter ordering in methodsتوی اندروید ممکنه ما متدی بنویسیم که از Context استفاده میکنه. در این صورت Context باید اولین پارامتر ورودی متدمون باشه.اگه متد ما نیاز به یک اینترفیس callback داره برعکسِ Context اون باید آخرین پارامتر ورودی متدمون باشه.مثال:// Context always goes first
public User loadUser(Context context, int userId);

// Callbacks always go last
public void loadUserAsync(Context context, int userId, UserCallback callback);2.2.12 String constants, naming, and valuesالمان های زیادی در اندروید وجود داره که برای استفاده از اونها ممکنه مجبور بشیم که ثابت های رشته ای تعریف بکنیم. حتی در برنامه های کوچک. مانند SharedPreferences، Bundle یا Intent باید اونها رو به شکل final static تعریف کنیم. و نام اونها رو باید به شکل پیشوند بنویسیم. این پیشوند ها باید به شکل زیر باشند.مثال:// Note the value of the field is the same as the name to avoid duplication issues
static final String PREF_EMAIL = &amp;quotPREF_EMAIL&amp;quot
static final String BUNDLE_AGE = &amp;quotBUNDLE_AGE&amp;quot
static final String ARGUMENT_USER_ID = &amp;quotARGUMENT_USER_ID&amp;quot

// Intent-related items use full package name as value
static final String EXTRA_SURNAME = &amp;quotcom.myapp.extras.EXTRA_SURNAME&amp;quot
static final String ACTION_OPEN_USER = &amp;quotcom.myapp.action.ACTION_OPEN_USER&amp;quot2.2.13 Arguments in Fragments and Activitiesزمانی که میخوایم دیتایی رو ازطریق intent یا bundle به اکتیویتی یا فرگمنت منتقل کنیم باید کلیدها برای دو مقدار متفاوت از قوانین بالا پیروی کنه.زمانی که یک اکتیویتی یا فرگمنت انتظار یک آرگومان رو داره، در این صورت باید یک متد public static برای تسهیل ایجاد intent یا فرگمنت مناسب بنویسیم.در مورد اکتیویتی ها این متد با اسم ()getStartIntent نوشته میشه. مانند کد زیر:public static Intent getStartIntent(Context context, User user) {
	Intent intent = new Intent(context, ThisActivity.class);
	intent.putParcelableExtra(EXTRA_USER, user);
	return intent;
}در مورد فرگمنت ها این متد ()newInstance نام داره و مثل کد زیر نوشته میشه:public static UserFragment newInstance(User user) {
	UserFragment fragment = new UserFragment();
	Bundle args = new Bundle();
	args.putParcelable(ARGUMENT_USER, user);
	fragment.setArguments(args)
	return fragment;
}نکته اول: این متدها باید بالای کلاس و قبل از متد ()onCreate قرار بگیرند.نکته دوم: اگه از روش های بالا استفاده کردیم کلید ها باید به صورت private تعریف بشند چون دیگه بیرون از کلاس به اونها نیازی نیست.2.2.14 Line length limitطول خطوط کد نباید از 100 کاراکتر بیشتر بشه. اگه کد ما بیشتر از اینه پس باید با دو روش زیر تقسیم بشه.استفاده از یک متغیر محلی یا یک متد.(ترجیحا)تقیسم کردن یک خط به چند خط زیر هم.در این مورد دو تا استثنا وجود داره که ممکنه از 100 کارکتر بیشتر بشه: خطوطی که امکان تقسیم شدن رو ندارند. مانند URL ها طولانی در کامنت هاتعریف ها مربوط به import ها و package ها.2.2.14.1 Line-wrapping strategiesبرای توضیح دادن line_wrap یک فرمول دقیق و مشخصی وجود نداره. اما تعداد کمی قانون متداول وجود داره.Break at operatorsبرای شکستن خطوط طولانی میتونیم اون رو قبل از عملگر ها بشکنیم مثل کد زیر:int longName = anotherVeryLongVariable + anEvenLongerOne - thisRidiculousLongOne
        + theFinalOne;Assignment Operator Exceptionیک قانون دیگه وجود داره که در اون میتونیم این شکستن رو بعد از عملگر مساوی(=) انجام بدیم. مثل کد زیر:int longName =
        anotherVeryLongVariable + anEvenLongerOne - thisRidiculousLongOne + theFinalOne;Method chain caseزمانی که چند متد به شکل زنجیر وار صدا زده میشن(در واقع وقتی داریم از builder ها استفاده میکنیم) میتونیم بعد از صدا زدن متد و قبل از کاراکتر (.) این شکستن رو انجام بدیم. مثل کد زیر:Picasso.with(context).load(&amp;quothttp://ribot.co.uk/images/sexyjoe.jpg&amp;quot).into(imageView);Picasso.with(context)
        .load(&amp;quothttp://ribot.co.uk/images/sexyjoe.jpg&amp;quot)
        .into(imageView);Long parameters caseوقتی یک متد پارامتر های زیادی داره و باعث طولانی تر شدن خط ما میشه ما باید در هر پارامتر و بعد از کاما(,) این شکستن رو انجام بدیم. مثل کد زیر:loadPicture(context, &amp;quothttp://ribot.co.uk/images/sexyjoe.jpg&amp;quot, mImageViewProfilePicture);
loadPicture(context,
        &amp;quothttp://ribot.co.uk/images/sexyjoe.jpg&amp;quot,
        mImageViewProfilePicture,
        clickListener);2.2.15 RxJava chains stylingدر استفاده از متدهای زنجیره ای rxjava باید قبل از نقطه(.) شکستن اتفاق بیوفته. مثل کد زیر:public Observable&lt;Location&gt; syncLocations() {
    return mDatabaseHelper.getAllLocations()
            .concatMap(new Func1&lt;Location, Observable&lt;? extends Location&gt;&gt;() {
                @Override
                 public Observable&lt;? extends Location&gt; call(Location location) {
                     return mRetrofitService.getLocation(location.id);
                 }
            })
            .retry(new Func2&lt;Integer, Throwable, Boolean&gt;() {
                 @Override
                 public Boolean call(Integer numRetries, Throwable throwable) {
                     return throwable instanceof RetrofitError;
                 }
            });
}2.3 XML style rules2.3.1 Use self closing tagsاگه یک المان در XML هیچ محتوایی در داخلش نداره پس باید اون رو در خودش ببندیم مانند کد زیر:کد خوب:&lt;TextView
	android:id=&amp;quot@+id/text_view_profile&amp;quot
	android:layout_width=&amp;quotwrap_content&amp;quot
	android:layout_height=&amp;quotwrap_content&amp;quot /&gt;کد بد:&lt;!-- Don\&#039;t do this! --&gt;
&lt;TextView
    android:id=&amp;quot@+id/text_view_profile&amp;quot
    android:layout_width=&amp;quotwrap_content&amp;quot
    android:layout_height=&amp;quotwrap_content&amp;quot &gt;
&lt;/TextView&gt;2.3.2 Resources namingآیدی ریسورس ها باید به شکل lowercase_underscore انجام بشه.2.3.2.1 ID namingآیدی باید به شکل پیشوند و با نام المان شروع بشه:مثال:&lt;ImageView
    android:id=&amp;quot@+id/image_profile&amp;quot
    android:layout_width=&amp;quotwrap_content&amp;quot
    android:layout_height=&amp;quotwrap_content&amp;quot /&gt;
&lt;menu&gt;
	&lt;item
        android:id=&amp;quot@+id/menu_done&amp;quot
        android:title=&amp;quotDone&amp;quot /&gt;
&lt;/menu&gt;2.3.2.2 Stringsنام رشته باید با یک پیشوند از بخشی که به اون تعلق داره شروع بشه. برای مثال: registration_email_hintیاregistration_name_hintاگه یک رشته به هیچ بخش مشخصی تعلق نداره پس باید از قوانین زیر پیروی کنه:2.3.2.3 Styles and Themesبرخلاف بقیه ریسورس ها استایل ها باید با قانون UpperCamelCase.نام گذاری بشن.2.3.3 Attributes orderingبه عنوان یک قاعده کلی باید سعی کنید attribute های مشابه رو با هم گروه بندی کنیم. یک راه خوب برای مرتب سازی اونها به شکل زیره:View IdStyleLayout width and layout heightOther layout attributes, sorted alphabeticallyRemaining attributes, sorted alphabetically2.4 Tests style rules2.4.1 Unit testsکلاسهای آزمون باید با نام کلاس مورد نظر مطابقت داشته باشند. مثلا اگه ما کلاس تستی داشته باشیم که مربوط به DataBaseHelper پس باید اسم کلاس تستمون DataBaseHelperTest باشه.متد تست با Test@ حاشیه نویسی میشه و باید با نام متدی که تست میشه شروع بشه و بعد پیشبینی و/یا رفتار مورد انتظار نوشته بشه.به مثال زیر نوجه کنید:Template: @Test void methodNamePreconditionExpectedBehaviour()Example: @Test void signInWithEmptyEmailFails()اگه اسم متد تست به اندازه کافی واضحه پس نیازی نیست به اضافه کردن پیشبینی و/یا رفتار مورد انتظار نیست.گاهی ممکنه کلاسی دارای متدهای بزرگ زیادی باشه، و اغلب نیازه که برای هر کدوم چندین تست نوشته بشه. در این صورت خوبه که کلاس های تست رو به چند کلاس مختلف تقسیم کنیم. برای مثال فرض کنید کلاسی به اسم DataManager وجود داره که دارای متدهای بزرگ زیادیه، پس ما میتونیم اونو به کلاس های تست DataManagerSignInTest ،DataManagerLoadUsersTest و ... تقسیم کنیم.2.4.2 Espresso testsمعمولا هر کلاس تست Espresso برای یک اکتیویتی نوشته شده پس نام اون باید با نام اکتیویتی تطبیق داشته باشه مثل: SignInActivityTestباید هنگام استفاده از Espressso هر متد رو در خط جداگانه ای نوشت.onView(withId(R.id.view))
        .perform(scrollTo())
        .check(matches(isDisplayed()))خب این مقاله تموم شد.امیدوارم که خوب ترجمه کرده باشم. لینک اصلی مقاله اینجاست و اینجاست.اگه جایی رو بد توضیح دادم یا اشتباه نوشتم ممنون میشم بهم بگین.</description>
                <category>Mahdi Sadeghi</category>
                <author>Mahdi Sadeghi</author>
                <pubDate>Tue, 25 Aug 2020 12:45:35 +0430</pubDate>
            </item>
            </channel>
</rss>