<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
    <channel>
        <title>نوشته های Mohsen Abedini</title>
        <link>https://virgool.io/feed/@mohsenabedini79ooo</link>
        <description>I am Android Developer</description>
        <language>fa</language>
        <pubDate>2026-06-16 16:05:05</pubDate>
        <image>
            <url>https://files.virgool.io/upload/users/267477/avatar/QEbtIY.jpeg?height=120&amp;width=120</url>
            <title>Mohsen Abedini</title>
            <link>https://virgool.io/@mohsenabedini79ooo</link>
        </image>

                    <item>
                <title>پرفورمنس در جت پک کامپوز ( بررسی در زمان کامپایل )</title>
                <link>https://virgool.io/AndroidGeeks/%D9%BE%D8%B1%D9%81%D9%88%D8%B1%D9%85%D9%86%D8%B3-%D8%AF%D8%B1-%D8%AC%D8%AA-%D9%BE%DA%A9-%DA%A9%D8%A7%D9%85%D9%BE%D9%88%D8%B2-%D8%A8%D8%B1%D8%B1%D8%B3%DB%8C-%D8%AF%D8%B1-%D8%B2%D9%85%D8%A7%D9%86-%DA%A9%D8%A7%D9%85%D9%BE%D8%A7%DB%8C%D9%84-kac5rno2armr</link>
                <description>در این مقاله به بررسی روش های افزایش پرفورمنس جت پک کامپوز در زمان کامپایل می پردازیم .ورژن کامپوز مورد بحث در این مقاله 1.3.0 (BOM 2022.10.00) است همچنین تمام report ها در حالت Release بررسی شده اند. (اگر تنها بحث پرفورمنس مد نظر شماست تا بخش inferring class stability از مقاله skip کنید)همونطور که مطلع هستید کامپوز هم مانند view system قبلی اندروید از یک لایف سایکل مشخص پیروی میکند . طبق تصویر بالا در لایف سایکل جت پک کامپوز در ابتدا Composition اتفاق می افتد در این مرحله یک گراف که node های آن شامل کامپوننت های کامپوز است به صورت in-memory جنریت می شود که در آینده در رابطه با آن به طور مفصل صحبت می کنیم .مرحله ی بعدی در لایف سایکل layout است . در این مرحله اندازه کامپوننت ها و محل قرار گیری آن ها در صفحه مشخص می شود (measure).نهایتا کامپوز وارد فاز Draw می شود که در این مرحله بر اساس گراف و همچنین measure آن ها ویو ها را در ترد Ui ترسیم می کند که بر روی اسکرین قابل مشاهده است . ( on draw)تا این جای کار با لایف سایکل کامپوز آشنا شده ایم . ما برای افزایش پرفورمنس کامپوز در دو سطح می توانیم عمل کنیم :سطح اول جایی است که گراف کامپوننت ها جنریت می شود (Composition) اگر در این سطح بتوانیم به تولید گراف optimize شده کمک کنیم می توانیم پرفورمنس را بالا ببریم .سطح دوم بعد از draw اولیه است . بعد از اینکه ویو ترسیم شد هر چه تعداد recomposition ها را پایین تر نگه داریم به پرفورمنس کامپوز کمک کرده ایم .در سطح دوم به کمک شمارش تعداد recpmposition ها و ایجاد تغییرات در compose هایی که مکررا و بی مورد recompose می شوند می توانیم اثر گذار باشیم . اما سطح دوم مورد بحث ما در این مقاله نیست .بنابراین در این مقاله ما قصد داریم به کمک کامپایلر کامپوز , گراف کامپوننت ها را بهینه سازی کنیم .اما قبل از این کار اجازه بدید تا اندکی با under the hood کامپوز بیش تر آشنا بشیم و ببینیم که کامپوز اصلا چه طور کار میکنه:کامپوز در بکگراند :وقتی یک فانکشن را به کمک انوتیشن composable@ علامت گذاری می کنید خروجی آن فانکشن به Unit تغییر کرده و مقادیری را به عنوان ورودی  آن فانکشن در نظر می گیرد (ورودی ها در composable function ها مهم است چه آن هایی که ما تعریف میکنیم چه آن هایی که کامپایلر تزریق میکند مانند composer). همچنین فانکشن قابلیت restartable پیدا می کند که یک امر مهم در recompostion است . وظیفه ی composable function ها آن است که پس از اجرا با دریافت مقادیر ورودی node هایی را ایجاد کنند و آن را در قسمتی از گراف کامپوننت ها emit کنند . در این جا به ذکر یک نکته می پردازیم :حاصل یک composable function یک یا تعدادی node است که هر وقت به آن احتیاج بود در گراف قرار می گیرد . به همین دلیل است که خروجی این فانکشن ها Unit است . این فانکشن ها برای اینکه بتوانند node را هر زمان که لازم بود به گراف ارسال کنند node را صراحتا return نمی کنند بلکه آن را emit می کنند.متن بالا را دوباره بخوانید در جایی از آن گفتیم که به اضافه ی آرگومان هایی که ما برای یک composable function معین می کنیم کامپایلر هم مقادیری را به عناون آرگومان برای آن ارسال می کند که از دید ما مخفی است .یکی از این مقادیر مهم composer است . مثال زیر را در نظر بگیرید :@Composable
fun NamePlate(name: String, lastname: String) {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(text = name)
        Text(text = lastname, style = MaterialTheme.typography.subtitle1)
    }
} کد بالا پس از کامپایل به صورت زیر تبدیل می شود :fun NamePlate(name: String, lastname: String, $composer: Composer&lt;*&gt;) {
    $composer.start(123)
    Column(modifier = Modifier.padding(16.dp), $composer) {
        Text(
            text = name,
            $composer
        )
        Text(
            text = lastname,
            style = MaterialTheme.typography.subtitle1,
            $composer
        )
    }
    $composer.end()
}همانطور که مشاهده می کنید composer به عنوان ورودی به فانکشن اصلی پاس داده می شود و ابتدا و انتهای composable function  را با توابع composer.start(123) و composer.end() مشخص می کندعلت این امر آن است که کامپوز سعی دارد یک read Scope برای خود ایجاد کند تا بتواند state ها را بخواند و در مورد آن ها تصمیم گیری کند . عدد پاس داده شده به تابع start یک عدد رندوم است و توسط کامپایلر انتخاب میشود تا از آن به عنوان identity هر scope استفاده کند .همانطور که مشاهده می کنید composer در تمامی child compose های موجود در بدنه ی NamePlate نیز پاس داده شده است .اما سوال مهم اینکه composer اصلا چیه ؟کامپوزر که به آن Calling Context نیز گفته می شود از بالا ترین تا پایین ترین node در گراف را طی می کند و از میان تمام لول های گراف در دسترس است . کامپایلر به کمک composer مشخص میکند که هر node در کجای گراف در حال کال شدن است.  به کمک  composer , کامپایلر یک قرارداد محکم را تبیین می کند :‌ تمامی کامپوزبل فانکشن ها فقط می توانند در کامپوزبل فانکشن های دیگر کال شوند.یا به عبارت دیگر تمامی کامپوزبل ها نیاز دارند تا context را کال کنند (به composer احتیاج دارند.) همچنین کامپایلر به کمک composer می تواند از هر composable اطلاعات ضروری مورد نیازش را دریافت کند و آن را آماده ی پراسس کند.این مفهموم که composable ها صرفا در composable های دیگر کال می شوند بسیار شبیه به همان suspend keyword است که در کروتین نیز بیان می شد . در واقع suspend هم به نوعی همان کاری را با فانکشن می کند که انوتیشن composable@  می کند . مثال زیر را در نظر بگیرید : suspend fun publishTweet(tweet: Tweet): Post = ...کد بالا توسط کاتلین کامپایلر به کد زیر تبدیل می شود:fun publishTweet(tweet: Tweet, callback: Continuation&lt;Post&gt;): Unitدر کد بالا callback از نوع Continuation همان چیزی است که تضمین می کند تمام suspend ها در suspend دیگر کال شوند.در این موارد می توان بحث های زیادی را مطرح کرد که از هدف این مقاله خارج است . بنابراین به سراغ ادامه ی مطالب می رویم :ران تایم و کامپایلر در کامپوز‌:تا اینجای مقاله مکررا در رابطه با compose compiler صحبت کردیم و ازش اسم بردیم اما کامپایلر و ران تایم در کامپوز چه هستن و چه کار میکنند ؟کامپوز در بکگراند از دو قسمت تشکیل شده : 1) کامپایلر : کامپایلر کامپوز قسمتی است که سورس کد نوشته شده توسط برنامه نویس را دریافت کرده و پس از بررسی صحت آن , گراف کامپوننت ها را جنریت می کند (در رابطه با کامپایلر به طور مفصل صحبت خواهیم کرد)2) ران تایم : گراف تولید شده توسط کامپایلر را که به صورت in-memory موجود است به عنوان ورودی به ران تایم داده می شود و ران تایم طبق آن تصمیم میگیرد که با ui چگونه برخورد شود در نهایت  ui روی پلتفرم مشخصی به دستورات ترسیم تبدیل می شود و آماده ی نمایش است .کامپوز کامپایلر :واضح است که کامپوز از شیوه ی meta programming برای بسیاری از قسمت های فریمورک استفاده می کند (annotations )در دنیای jvm برای تعریف یک annotation processor به طور معمول از kapt یا ksp استفاده می شود .اما در جت پک کامپوز برای این که بتواند به برخی از محدودیت های کامپایلر کاتلین غلبه کند از kapt یا ksp استفاده نشده است بلکه به کمک توسعه ی compose compiler کاستوم توانسته نه تنها محدودیت های کامپایلر کاتلین را دور بزند بلکه با رجیستر شدن در کنار کامپایلر کاتلین بسیاری از اعمال compile time را در کنار kotlin compiler به خوبی انجام دهد . (به طور مثال از محدودیت های کامپایلر کاتلین می توان به الزام کوچک بودن حرف اول نام توابع اشاره کرد که در کامپوز نقض میشود) این که کامپوز از کامپایلر شخصی خودش استفاده می کند به وضوح توانسته سرعت کار آن را افزایش دهد و همچنین همکاری خوبی را با کامپایلر کاتلین آغاز کند .ساختار کامپایلر :کامپوز کامپایلر مانند هر کامپایلر دیگری از دو فاز تشکیل شده است :فاز front end : در این فاز کلمه به کلمه ی کد توکنایز می شود و به روش های مختلف استاتیک بررسی می شود تا از صحت کد های نوشته شده اطمینان یابد .به طور مثال در این فاز از کامپایلر کامپوز قسمت های انوتیت شده با composable@ پیدا می شوند سپس بررسی می کند که آیا ترکیب کد ها با هم مطابق موارد مورد انتظار هست و شرایطی را نقض نمی کند سپس در صورت وجود مشکل ارور ها یا وارنینگ هایی را report می کند . این وارنینگ ها و ارور ها در حین کد زدن برنامه نویس افشا می شوند و برنامه نویس را از خطا ها آگاه می کنند . مثلا فرض کنید از یک تابع composable در یک بلاک غیر کامپوزی استفاده کرده اید در این صورت کامپایلر, در جا برای شما ارور هایی را report می کند . یا یکی دیگر از کار هایی که در این فاز اتفاق می افتد این است که بررسی می شود آیا کامپوز کامپایلر با ورژن کاتلین تعریف شده در پروژه compatible هست یا خیر ؟ زیرا هر کامپایلر کامپوز تنها با ورژن های خاصی از کاتلین کار میکند.فاز بکند : در فاز فرانت در صورتی که صحت سورس کد تایید شود فاز بکند مشغول به کار می شود و از روی سورس کد اولیه , کد های ماشین مقصد را تولید می کند .در کامپایلر کامپوز این همان مرحله ی است که در آن کد ها و boilerplate های مربوط به گراف کامپوننت ها تولید می شود .نکته ای که در این جا حایز اهمیت است این است که در فاز بکند علاوه بر تبدیل سورس کد اولیه به کد های مورد نیاز پلتفرم , کامپوز کامپایلر می تواند سورس کد را برای اهداف خاصی دستکاری کند . نحوه ی کار به این صورت است که کامپوز کامپایلر سورس هارا دریافت کرده و از آن ها کد های میانی IR یا  intermediate representation می سازد سپس در این جا قابلیت آن را دارد که IR را ویرایش کند و تغییرات مد نظرش را به آن اضافه کند . در نهایت از کد های میانی , کد مورد نیاز پلتفرم را میسازد .به طور مثال برخی از تغییراتی که کامپایلر روی IR اعمال می کند :همان composer که در تمامی کامپوزبل ها تزریق می شد .کد های مورد نیاز ایجاد قابلیت preview و live literal در حالت دیباگ اطلاق stability به کامپوزبل ها (تمام مواردی که در این مقاله تا کنون مطرح کردیم برای این بود که به این جا برسیم!!)برخی از کد های boilerplate برای جلوگیری از ردیابی IRو....در نهایت کد نهایی تولید شده توسط کامپایلر برای پردازش به ران تایم تحویل داده میشود .کامپوز ران تایم:ران تایم وظیفه ی پراسس گراف و اماده سازی گراف برای تبدیل شدن به actual ui را بر عهده دارد .اما در این جا ما یک نکته از مهندسی نرم افزار بیان می کنیم : همونطور که می دونید کار ها در زمان کامپایل سریع تر از ران تایم انجام میشن . ران تایم باید خط به خط تفسیر کند و بر اساس شرایط محیط اجرا تصمیم گیری های حیاتی را انجام دهد .اگر بخواهیم یک مثال از این قضیه بیان کنیم میتونیم به تفاوت های Hilt , Koin در ماجرای تزریق وابستگی ها اشاره کنیم (البته که مقایسه این دو به دلیل نحوه عملکردشون در ایجاد وابستگی صحیح نیست)اما هیلت به دلیل اینکه وابستگی ها رو در زمان کامپایل کد جنریت می کنه سرعت بالا تری در ران تایم داره اما زمان بیلد مارو افزایش می ده ,  در حالی که Koin به دلیل عملکردش در ران تایم , پروفورمنس پایین تری نسبت به هیلت داره .بنابراین ما هر چه بتوانیم از وظایف ران تایم را به زمان کامپایل بیاوریم , پرفورمنس را افزایش داده ایم  .جت پک کاموز نیز از این اصل برای افزایش پرفورمنس استفاده می کند .Inferring class stability concept *این بخش را با یک جمله کلیدی شروع می کنیم :‌کامپایلر کامپوز می تواند مواردی را برای ران تایم تدریس کند.همانطور که قبل تر مطرح کردیم هر چه از وظایف ران تایم را بتوانیم کم تر کنیم می توانیم سرعت را بهبود ببخشیم . بنابراین در این جا کامپایلر وارد عمل می شود و مواردی مانند  stability  را در لول کامپایل برای کامپوز ران تایم مشخص میکند.اکنون به توضیح چند مبحث مهم می پردازیم :ریکامپوزیشن (restartable - recomposition) :تمامی composable function ها به طور پیشفرض restartable هستند . به این معنا که می توانند به دفعات اجرا شوند . این امر باعث می شود تا فرایند recomposition که برای تغییر یک node از قبل ایجاد شده به کار می رود معنا پیدا کند.recompositionکامپوزبل فانکشن های readOnly :همانطور که در بالا اشاره شد تمام کامپوزبل فانکشن ها به صورت پیشفرض restartable هستند و توانایی recomposition دارند . اما ما به کمک انوتیشن NonRestartableComposable@ در بالای composable ها می توانیم قابلیت restart را از فانکشن بگیریم در این صورت فانکشن نمی تواند خودش را recompose کند و سیگنال ریکامپوز را صرفا از والد خود دریافت می کند. در رابطه با کاربرد این نوع از توابع در آینده صحبت خواهیم کرد. (به طور مثال از از این توابع به stringResource میتوان اشاره کرد )اسمارت ریکامپوزیشن (smart recomposition - skippable) :در تصویر بالا بلاک counter ریکامپوز می شود اما آیا بلاک های دیگر مانند TopAppBar هم باید ریکامپوز شوند؟ در مورد فرزندان counter چه؟ آیا به ریکامپوز شدن همه ی آن ها احتیاج است؟smart recompositionمشخصا خیر . در واقع این همان کاری است که smart recomposition انجام می دهد . smart recomposition یکی از مصادیق مهم قدرت در جت پک کامپوز است .* اسمارت ریکامپوزیشن به این معناست که recomposition در کامپوز هایی که ورودی های آن ها تغییری نداشته و آن ورودی ها stable هستند را skip کرده و نادیده بگیریم .*کامپوز ران تایم با بررسی 2 شرط بالا تصمیم می گیرد که آیا باید ریکامپوزیشن skip شود یا خیر.آرگومان های stable و unstableما در رابطه با فانکشن های restartable و skippable و readOnly صحبت کردیم . اکنون نوبت آن است تا پیرامون آرگومان های این توابع صحبت کنیم:ارگومان های stable : این نوع از آرگومان ها در طی فرایند ریکامپوزیشن ران تایم را از تغییر مقدارشان مطلع میکنند در نتیجه ران تایم میتواند تصمیم بگیرد که آیا recompostion انجام شود یا خیر ارگومان های unstable : از تغییر وضعیتشان چیزی را به ران تایم اطلاع نمی دهند.پس در این جا به goal مقاله رسیدیم :کامپایلر به طور خودکار وضعیت توابع کامپوز ما را به لحاظ restartable , skippable و آرگومان هارا به لحاظ stable , unstable بودن مشخص میکند . توابع به صورت نرمال به صورت restartable skippable مارک می شوند (هم قابلیت recompose شدن دارند و هم قابلیت smart recomposition) و همچنین آرگومان ها به صورت Stable .اما شرایطی وجود دارد که اگر توسط ما نقض شود کامپایلر , آرگومان را unstable میکند و در نتیجه قابلیت skippable از دست می رود (smart recomposition در ران تایم انجام نمی شود)در این جا هدف این است تا این موارد را پیدا کرده و آن هارا اصلاح کنیم.از کامپوز نسخه 1.2.0 فیچر composable metrics به کامپایلر اضافه شد که ما می توانیم به کمک بررسی آن متوجه شویم که کامپایلر در مورد composable function های ما چه تصمیمی گرفته است . هدف ما از این جا به بعد آن است که این ریپورت ها را بررسی کنیم و متوجه شویم چه عواملی می تواند باعث unstable شدن input های ما شود و اینکه چگونه آن ها را اصلاح کنیم .در ابتدا باید metrics ها را فعال کنیم برای این کار کد زیر را به build.gradle سطح اپ اضافه می کنیم : kotlinOptions {    //jvmTarget = &quot;17&quot;    // following lines (freeCompilerArgs) to enable compose-metrics    freeCompilerArgs += listOf(        &quot;-P&quot;,        &quot;plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=&quot; + project.buildDir.absolutePath + &quot;/compose_metrics&quot;    )    freeCompilerArgs += listOf(        &quot;-P&quot;,        &quot;plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=&quot; + project.buildDir.absolutePath + &quot;/compose_metrics&quot;    )} در گام بعدی بعد از سینک کردن gradle باید کامند زیر را ران کنیم تا گزارش ها generate شوند :(دو بار کلید CTRL را در محیط اندروید استادیو بزنید تا بتوانید کامند را ران کنید)./gradlew assembleRelease -PNAME.enableComposeCompilerReports=trueبعد از (p-) به جای NAME اسم اپلیکیشن خود را که در پروژه تعریف کرده اید قرار بدید.همانطور که مشاهده می کنید این کامند در مود ریلیز بیلد می گیرد .  اگر در حالت دیباگ ( assembleDebug) بیلد بگیریم ممکن است نتایج report ها صحیح نباشد (زیرا مطابق آن چه گفته شد در حالت دیباگ مواردی برای live literal , preview به کد اضافه می شوند که میتواند سبب اشتباه در گزارش ها شوند. اما در حالت ریلیز, R8 کد های اضافه را از سورس کد پاک می کند) بعد از اتمام بیلد در دایرکتوری بیلد اپلیکیشن پوشه ی compose-metrics ایجاد می شود که شامل گزارش هایی است که به بررسی هر کدام خواهیم پرداخت :app_release-module.json این فایل شامل گزارش هایی آماری از تمام کامپوزبل فانکشن های ما است:{
 skippableComposables: 147,
 restartableComposables: 156,
 readonlyComposables: 0,
 totalComposables: 168,
}البته که اطلاعات موجود در این فایل بیش تر از این مقدار است اما ما مهم ترین آن ها را آورده ایم .تعداد تمام کامپوزبل فانکشن ها و تعداد موارد skippable , restartable و readOnly در این فایل مشخص است .یک نکته ی مهم که باید در نظر داشته باشیم آن است که :اگر تعدادی از کامپوزبل های ما skippable نباشند الزاما به معنای یک نکته منفی نیست . ما حداکثر تلاش خود را برای skippable شدن فانکشن ها می کنیم . اما شاید در مواردی کنترل فانکشن از دست ما خارج باشد.ممکن است در ریپازیتوری های رسمی گوگل هم مواردی را بیابید که skippable نباشند .app_release-composables.txtrestartable skippable scheme([androidx.compose.ui.UiComposable]) fun MyScreen(
  stable title: String
  stable content: String
  stable itemCount: Int
)در این فایل که یک ریپورت بسیار مهم است وضعیت توابع کامپوز مشاهده می شود . با تحلیل این فایل میتوان مشکلات موجود در توابع و آرگومان های کامپوز را پیدا کرد.(اکنون با مقایسه تصویر و کد آورده شده باید بتوانید فانکشن مشکل دار را تشخیص دهید )app_release-composables.csvنتایج همان فایل بالا را به صورت طبقه بندی شده و در غالب csv به نمایش می گذارد.app_release-classes.txt این فایل وضعیت stable بودن کلاس های پاس داده شده به کامپوزبل فانکشن ها را مشخص میکند.stable class InherentlyStableClass {
 stable val publicStableProperty: String
 stable val privateUnstableProperty: String
 &lt;runtime stability&gt; = Stable
}اکنون stability آرگومان ها را مورد بحث قرار می دهیم :تمام تایپ های اولیه (Int,Boolean,Float, ... ) stable اند.استرینگ ها نیز stable اند.مثال زیر رادر نظر بگیرید :@Composable
    fun StableComposeWithPrimitiveType(
        title: String,
        content: String,
        itemCount: Int
    )به دلیل stable بودن تمام آرگومان ها, فانکشن skippable است .restartable skippable scheme([androidx.compose.ui.UiComposable]) fun
StableComposeWithPrimitiveType(
  stable title: String
  stable content: String
  stable itemCount: Int
)تمام var ها unstable اند .مثال زیر را در نظر بگیرید :data class InherentlyUnstableClass(var text: String)اگر چه text از نوع String است اما به دلیل آنکه به صورت var تعریف شده است مطلقا به عنوان unstable در نظر گرفته میشود . این دیتا کلاس به دلیل وجود یک آرگومان unstable , unstable تلقی شده و اگر به هر کامپوزبل فانکشنی پاس داده شود آن فانکشن قابلیت skip را از دست میدهد.unstable class InherentlyStableClass {
  unstable var text: String
}* برای اصلاح این امر کافیه تا به جای var از val استفاده کنیدdata class InherentlyStableClass(val text: String)کالکشن ها (List,Set,Map ...) unstable اند.@Composable
fun UnstableComposeWithList(
    title: String,
    content: String,
    itemCount: List&lt;Int&gt;
)در مثال بالا تنها وجود یک آرگومان unstable مانند list برای نقض skippable بودن کل تابع کافیست .restartable scheme([androidx.compose.ui.UiComposable]) fun UnstableComposeWithList(
  stable title: String
  stable content: String
  unstable itemCount: List&lt;Int&gt;
)* برای اصلاح این امر کافیه که از Mutable Collection ها مانند (MutableList) به جای List استفاده کنیم .فلو و (observable ها) unstable اند .فلو ها در هر شرایطی اگر به عنوان آرگومان در کامپوزبل فانکشن ها استفاده شوند unstable خواهند بود. زیرا هنگامی که یک مقدار جدید را emit می کنند ران تایم را notify نمی کنند .@Composable
fun UnstableWithFlow(events: Flow&lt;String&gt;) {
    LaunchedEffect(Unit) {
        events.collect {
            // Process the events
        }
    }
}restartable fun UnstableWithFlow(
  unstable events: Flow&lt;String&gt;
  unused unstable &lt;this&gt;: MainActivity
)* برای اصلاح این امر فقط در شرایطی که مطلقا به آن ها نیاز دارید از flow به عنوان آرگومان استفاده کنید .لامبدا ها به صورت پیشفرض stable اند.@Composable
fun stableLambdaScreen(
    : (Date) -&gt; Unit = { }
) {}در مثال بالا علارغم unstable بودن ابجکت date , به دلیل آن که با یک stable پوشانده شده است , آرگومان  stable تلقی می شود . (دلیل این امر را در قسمت های بعدی بررسی خواهیم کرد)restartable skippable scheme(&amp;quot[androidx.compose.ui.UiComposable]&amp;quot) fun stableLambdaScreen(
   stable : Function1&lt;Date, Unit&gt;? = @static { it: Date -&gt;
}* توجه شود که همچنان شرایطی وجود دارد که ممکن است سبب unstable شدن لامبدا فانکشن ها شود (این شرایط وابسته به ورژن کامپوز ممکن است متفاوت باشند . بنابراین همواره گزارش های کامپایلر را بررسی کنید)تمام آبجکت هایی که در ماژول های غیر کامپوزی اند unstable تلقی می شوند.@Composable
fun UnstableWithDateType(date: Date) {
    Text(text = date.time.toString())
}در مثال بالا آرگومان Date به دلیل آنکه از یک ماژول غیر کامپوزی است (java) باعث unstable شدن آرگومان میشود .restartable scheme([androidx.compose.ui.UiComposable]) fun UnstableWithDateType(
  unstable date: Date
  unused unstable &lt;this&gt;: MainActivity
)در پروژه های اندرویدی چنین شرایطی ممکن است به دفعات پیش بیاید (به طور مثال در زمان هایی که مواردی از ماژول data به لایه ی ui نشت می کند) که برای اصلاح آن دو روش بسیار مهم وجود دارد : *روش اول : استفاده از انوتیشن  NonRestartableComposable در بالای Composable Function @NonRestartableComposable
@Composable
fun StableWithNonRestartable(date: Date) {
    Text(text = date.time.toString())
}scheme([androidx.compose.ui.UiComposable]) fun StableWithNonRestartable(
  unstable date: Date
)با بررسی گزارش بالا متوجه چه تغییری می شوید ؟ آیا مشکل unstable بودن Date حل شده است؟ مشخصا خیر . پس چه چیزی تغییر داشته است ؟با بررسی گزارش متوجه می شویم که فانکشن دیگر restartable نیست (قابلیت recompose ندارد و ReadOnly است) . در واقع  ما صورت مسیله را به صورت کامل پاک کرده ایم . وقتی که فانکشن restartable نباشد skippable بودن یا نبودن معنایی پیدا نمی کند که اکنون بخواهیم پیرامون وضعیت آرگومان بحث کنیم.وقتی که از انوتیشن NonRestartableComposable استفاده می کنیم کامپایلر boilerplate های مورد نیاز برای recompose شدن فانکشن را جنریت نمی کند بنابراین فانکشن قابلیت recompose شدن خودش را از دست می دهد و سیگنال ریکامپوز را تنها از parent خودش دریافت میکند . استفاده از این انوتیشن به صورت گسترده توصیه شده نیست و برای کامپوزبل فانکشن هایی مناسب است که کوچک هستند و logic خاصی را هندل نمی کنند بنابراین می توانند قابلیت self invalidate یا self recomposition را به صورت پیشفرض نداشته باشند و تنها از طریق parent ریکامپوز شوند.بنابراین این روش , در بسیاری از موارد کاربردی نیست و استفاده ی اشتباه از آن می تواند باعث بروز رفتار های ناخواسته شود. روش دوم : پوشاندن آرگومان unstable با یک wrapper class و مارک کردن آن با انوتیشن های Stable@ یا Immutable@ مثال زیر را در نظر بگیرید :@Stable
data class StableDateHolder(val date: Date)

@Composable
fun StableWithWrapper(date: StableDateHolder) {
    Text(text = date.time.toString())
}restartable skippable scheme(&amp;quot[androidx.compose.ui.UiComposable]&amp;quot) fun StableWithWrapper(
  stable date: StableDateHolder
) در این مثال به جای پاس دادن مستقیم آرگومان Date , آن را با یک data calss پوشانده ایم سپس data class را به عنوان Stable مارک کرده ایم  و این data class را به جای Date به عنوان آرگومان پاس دادیم , همانطور که مشاهده میکنید بعد از این تغییرات  StableDateHolder به عنوان stable تلقی می شود .همین کار را به کمک انوتیشن Immutable نیز می توان انجام داد .@Immutable
data class ImmutableDateHolder(val date: Date)

@Composable
fun StableWithWrapper(date: ImmutableDateHolder) {
    Text(text = date.time.toString())
}restartable skippable scheme([androidx.compose.ui.UiComposable]) fun StableWithWrapper(
  stable date: ImmutableDateHolder
  unused unstable &lt;this&gt;: MainActivity
) اما تفاوت این دو انوتیشن چیست و چرا موجب stable شدن آرگومان ها می شوند؟این دو انوتیشن قول های محکمی (strong promise) برای کامپایلر کامپوز هستند که به کمک آن ها برنامه نویس تضمین میکند که کامپایلر میتواند آرگومان های مارک شده را stable تلقی کند . انوتیشن Immutable :تضمین میکنیم که مقدار آرگومان در ران تایم قابل تغییر نیست (immutable است) بنابراین ران تایم اصلا احتیاجی به بررسی تغیررات آرگومان ندارد.انوتیشن Stable:تضمین میکنیم که با اینکه مقدار آرگومان در زمان ران تایم می تواند عوض شود , در صورت عوض شدن مقدار, حتما ران تایم را از تغییر اتفاق افتاده  notify میکنیم . بنابراین ران تایم همواره از تغییرات آگاه هست .بنابراین هر دوی این انوتیشن های سبب می شوند تا آرگومان stable تلقی شود با این تفاوت که Immutable در مواردی استفاده می شود که مطمین هستیم مقدار آرگومان در ران تایم عوض نمی شود در حالی که در Stable ممکن است مقدار تغییر کند .اما یک نکته  : اگر از این انوتیشن ها به صورت اشتباه استفاده شود چه اتفاقی می افتد ؟ مثال زیر را در نظر بگیرید :@Immutable
data class ImmutableDateHolder(val date: MutableState&lt;Date&gt;)

@Composable
fun StableWithImmutableWrapData(date: ImmutableDateHolder, : () -&gt; Unit) {
    Text(
        text = date.date.value.time.toTimeDateString(),
        modifier = Modifier.clickable { () })
}کلاس  ImmutableDateHolder به صورت Immutable مارک شده است اما مقدار Date از نوع  MutableState مشخص شده است تا بتوان مقدار آن را در ران تایم با کلیک روی Text عوض کرد .همانطور که قبلا گفته شد در مواردی که از Immutable@ استفاده میشود مقدار آرگومان نباید در ران تایم عوض شود اما در این مثال که این شرایط نقض شده است پس از اجرا چه اتفاقی می افتد؟هیچ اتفاقی نمی افتد !! برنامه مطابق انتظار اجرا می شود .طبق گفته ی داکیومنت کامپوز استفاده ی نادرست از این انوتیشن ها ممکن است باعث رخ دادن رفتار های غیر منتظره شود . در مثال ما ui به صورت صحیح رفتار می کند اما ممکن است در سناریو های پیچیده تر منجر به مشکل شود بنابراین از این انوتیشن ها به صورت صحیح استفاده کنید .نکته :این انوتیشن ها یک آرگومان unstable را به ذات stable نمی کنند (Date هنوز هم unstable است) بلکه صرفا قرار داد ما با کامپایلر را تغییر می دهند . هنگام استفاده از این انوتیشن ها, ما به عنوان توسعه دهنده مواردی را برای کامپایلر تضمین میکنیم (promise می دهیم) بنابراین در استفاده از آن ها باید مراقب بود. نکته مهم 1 : همه ی composable ها را الزاما نباید skippable کرد.گاهی از اوقات تلاش برای skippable کردن همه ی composable ها خود می تواند باعث ایجاد مشکلات performance شود و یا باعث شود تا کد دیگر maintainable نباشد و یا هزینه زیادی را در زمان توسعه به پروژه اعمال کند.مثال زیر را در نظر بگیرید :@Composable
inline fun UnstableWithInline() {}اگر یک composable function را به صورت inline مارک کنیم باعث میشود تا composable در کامپایلر به صورت inline تلقی شود و قابلیت skip را از دست بدهد در این شرایط حتی کامپایلر نسبت به مشکلات پرفورمنس این تابع وارنینگ هایی را صادر می کند . در حالی که اگر کد های توابع Column و Row یا Box را بررسی کنید متوجه می شوید که به صورت inline مارک شده اند .public inline fun Box(modifier: androidx.compose.ui.Modifier)مگر inline باعث مشکلات پرفورمنس در کامپایلر کامپوز نمیشود ؟‌ پس چرا طراحان فریمورک تصمیم گرفته اند تا توابع Box , Column به صورت inline پیاده سازی شود ؟پاسخ این است که در بحث پرفورمنس دو عامل موثر است : کامپایلر و ران تایم در مورد inline درست است که در کامپایلر دچار مشکل هستیم اما پس از محاسبات توسط تیم کامپوز احتمالا در ران تایم باعث بهبود چشم گیری می شده است که حتی به issue های کامپایلر نیز غلبه می کرده است .این توسط طراحان قابل محاسبه بوده است اما برای ما خیر. بنابراین گاهی از اوقات تلاش برای رفع مشکلات کامپایلر به صورت افراطی ممکن است ما را درگیر چالش هایی در زمان ران تایم کند.نکته :‌ به علت inline بودن Box , Column و Row . آن ها read scope مختص خودشان را ندارند به این معنا که توانایی تولید و خواندن State ها را ندارند و تنها از read scope موجود در parent شان استفاده میکنند . هنگامی که یک state تغییر میکند آن ها recomposition را به نزدیک ترین scope ممکن منتقل می کنند. این نکته باعث افزایش پرفرومنس ران تایم می شود اما همچنان می تواند باعث ایجاد مشکلاتی برای ما بشود  (در انتهای مقاله در رابطه با این موارد بیش تر صحبت می کنیم ) در این جا ما شرایط کلی را مطرح میکنیم که skippable بودن مزیت خاصی ندارد . حتی باعث می شود تا نگهداری کد نیز سخت تر باشد :کامپوزبلی که اصلا یا غالبا ریکامپوز نمی شود .کامپوزبلی که تنها کامپوزبل های قابل skip را صدا میکند (خودش نیازی به skippable بودن ندارد)کامپوزبلی که آرگومان های بسیار زیاد با پیاده سازی های سنگینی دارد . در چنین شرایطی تلاش برای stable کردن همه ی آرگومان ها ممکن است هزینه سنگین تری نسبت به recompostion داشته باشد. نکته مهم 2 : نتایج حاصل از گزارش ها ممکن است در ورژن های مختلف کامپوز متفاوت باشد . من در ابتدای مقاله ورژن کامپوز خود را ذکر کردم . دلیل آن , اینست که در هر ورژن از کامپوز, کامپایلر آن نیز ممکن است قدرتمند تر شود. بنابراین ممکن است مواردی که در ورژن های قبل unstable در نظر گرفته می شدند در ورژن های جدید stable تلقی شوند .من در این مقاله سعی کردم تا مواردی که نسبت به وضعیت stability آن ها تردید وجود داشت را ذکر نکنم .بنابراین لازم است برای هر پروژه ی کامپوزی خود metrics هارا فعال کنید و وضعیت فانکشن های خود را بررسی کنید .Dynamic expressions in default parameterهنگامی که از default parameter ها استفاده می کنیم (x:Int = 0) کامپوز احتیاج دارد کد هایی را برای بررسی مقدار و تغییرات آن ها جنریت کند .دومین چیزی که در گزارش ها باید به دنبال آن باشیم static یا dynamic بودن هنگام استفاده از default parameter هاست .به کلید واژه ی static که در مثال لامبدا ها آوردیم توجه کنید :restartable skippable scheme([androidx.compose.ui.UiComposable]) fun stableLambdaScreen(
   stable : Function1&lt;Date, Unit&gt;? = @static { it: Date -&gt;
}ما در موارد بسیاری از default parameter ها استفاده می کنیم .دیفالت پارامتر ها ممکن است از composable  یا non-composable کد ها بیاید (به طور مثال مقدار false از non-composable است اما modifier از composable است )هنگامی که مقدار پارامتر دیفالت ممکن است در ران تایم تغییر کند کامپایلر آن ها را با dynamic@ مارک می کند (مانند مقادیر حاصل شده از کد های کامپوزی -&gt; به علت restartable بودن این کد ها مقدار دیفالت ممکن است در زمان ران تایم تغییر کند )در حالی که اگر مقدار پارامتر دیفالت در ران تایم قابل تغییر نباشد کامپایلر آن را static مارک می کند .از آن جا که قبل تر پیرامون کاهش وظایف ران تایم و تاثیر آن بر پرفورمنس صحبت کردیم هر چه دیفالت پارامتر های ما static باشند پرفورمنس بالا تری حاصل می شود .به مثال زیر توجه کنید :@Composable
fun DynamicDefaultParams(
    color: Color = MaterialTheme.colorScheme.background
) {}به دلیل اینکه مقدار دیفالت color از یک کلاس کامپوزی می آید این پارامتر dynamic است .restartable skippable fun UnstableWithDynamicDefaultParams(
   stable color: Color = @dynamic MaterialTheme.colorScheme.background
)*داینامیک یا استاتیک بودن روی skippable بودن فانکشن ها تاثیری ندارد .در دو حالت دیفالت پارامتر ها dynamic می شوند و برای static کردن آن ها راه حلی وجود ندارد:۱) در کد های کامپوزی (مانند MaterialTheme)۲) در  observable ها.(مانند Mutable State) -&gt; زیرا در ران تایم قابل تغییر اند@Composable
fun DynamicDefaultParams(
    mutableState: MutableState&lt;Int&gt; = mutableStateOf(10),
) {}در این دو حالت راهی برای جلوگیری از dynamic شدن مقادیر نداریم . اما شرایطی وجود دارد که می توانیم مقدار dynamic شده را static کنیم به مثال زیر توجه کنید :@Composable
fun AnimatedVolumeLevelBar(
    barWidth: Dp = 2.dp,
    gapWidth: Dp = barWidth
) {}restartable skippable fun AnimatedVolumeLevelBar(
  stable barWidth: Dp = @static 2.dp
  stable gapWidth: Dp = @dynamic barWidth
)در مثال بالا مقدار barWidth به علت قابل تغییر نبودن static است اما مقدار gapWidth به علت وابستگی به مقدار barWidth توسط کامپایلر dynamic تلقی می شود .به منظور اصلاح این مشکل می توان از کد زیر استفاده کرد :companion object{
    const val DEFAULT_WIDTH = 2
}
@Composable
fun AnimatedVolumeLevelBar(
    barWidth: Dp = DEFAULT_WIDTH.dp,
    gapWidth: Dp = DEFAULT_WIDTH.dp
) {}restartable skippable fun AnimatedVolumeLevelBar(
   stable barWidth: Dp = @static Companion.DEFAULT_WIDTH.dp
   stable gapWidth: Dp = @static Companion.DEFAULT_WIDTH.dp
)* در واقع تعریف مقادیر درcompanion (static) می تواند یک روش موثر در static کردن برخی از مقادیر dynamic باشد.*اکنون شما فکر می کنید که فقط ما از نکته ی بالا آگاه هستیم ؟ خیر تیم کامپوز نیز از این نکته آگاه هست.آن ها با قرار دادن برخی از composable های پر کاربرد در بلاک companion آن ها را استاتیک کرده اند .به مثال زیر توجه کنید :@Composable
fun AnimatedVolumeLevelBar(
    modifier: Modifier = Modifier,
    color: Color = Color.White
) {}restartable skippable fun AnimatedVolumeLevelBar(
   stable modifier: Modifier? = @static Companion
   stable color: Color = @static Companion.White
)علارغم کامپوزی بودن آرگومان های modifier و Color.White آن ها به صورت static مارک شده اند . علت این امر آن است که تیم کامپوز آن ها را در بلاک companion قرار داده است (به کلید واژه ی Companion در گزارش توجه کنید )تا این جای کار با تاثیر companion بر static شدن دیفالت پارامتر ها آشنا شدیم اما این تنها روش موجود نبود مثال زیر را در نظر بگیرید :@Composable
fun DefaultParameters(
    str: String = stringResource(id = R.string.app_name)
) {}همانطور که قبلا گفته شد دیفالت های کامپوزی dynamic هستند بنابراین stringResource باعث dynamic شدن دیفالت پارامتر می شود .restartable skippable fun DefaultParameters(
   stable str: String? = @dynamic stringResource(string.app_name, $composer, 0)
)حالا فرض کنید تابع را به صورت زیر بازنویسی کنیم :@Composable
fun DefaultParameters(
    str: Int = R.string.app_name
) {
val appName = stringResource(id = str)
}به دلیل آن که مقدار دیفالت پارامتر از کلاس R که در زمان کامپایل جنریت می شود گرفته شده است همچنان این پارامتر dynamic است :restartable skippable fun DefaultParameters(
   stable str: Int = @dynamic string.app_name
)اما اگر این کد را به صورت زیر تغییر دهیم چه اتفاقی می افتد؟fun getString(): Int {
    return R.string.app_name
}

@Composable
fun DefaultParameters(
    str: Int = getString()
) {}هیچ اتفاقی نمی افتد هنوز مقدار خروجی تابع getStringId به صورت dynamic است restartable skippable fun DefaultParameters(
   stable str: Int = @dynamic getString()
)تا این جا هیچ تغییر مثبتی ندیدیم ! اما اجازه بدید تا نگاهی به کد انوتیشن Stable@ که قبلا پیرامون آن صحبت کردیم بیندازیم :همانطور که مشاهده می کنید این انوتیشن روی (کلاس ها , فانکشن ها , پراپرتی ها) قابل اعمال است اگر روی فانکشن getString این انوتیشن را اعمال کنیم چه اتفاقی می افتد؟@Stable
fun getString(): Int {
    return R.string.app_name
}

@Composable
fun DefaultParameters(
    str: Int = getString()
) {}restartable skippable fun DefaultParameters(
   stable str: Int = @static getString()
)بله همانطور که مشاهده میکنید در این جا دیفالت پراپرتی static شد . این از خواص دیگر انوتیشن Stable@  است.* توجه کنید که من در این جا توصیه نکرده ام که از این به بعد هرگاه به دیفالت آرگومان با مقدار string احتیاج داشتید آن را به صورت int از یک فانکشن که stable مارک شده است return کنید بلکه صرفا روشی را به جهت static کردن مقادیر dynamic معرفی کرده ام .شاید شما ترجیح بدهید تا برای تمام string ها از stringResource استفاده کنید و به هیچ مشکل پرفورمنسی بر نخورید . شرایط پروژه شما و میزان استفاده از element ها مشخص میکند که شما چه قدر و روی چه مواردی باید حساس باشید .این مورد در رابطه با MaterialTheme و تمام dynamic های دیگر صادق است .ما تا این جای مقاله بحث کاملی پیرامون پرفورمنس در کامپوز داشته ایم و در رابطه با stable-unstable در آرگومان ها و static-dynamic در دیفالت پارامتر ها صحبت مفصلی داشتیم . اکنون به انتهای مقاله رسیدیم اما همانطور که در میانه ی مقاله قول دادیم در رابطه با مشکل inline در box , column صحبت خواهیم کرد :read scope issue in box , column , rowدر قسمت inline در مقاله گفتیم : نکته :‌ به علت inline بودن Box , Column و Row . آن ها read scope مختص خودشان را ندارند به این معنا که توانایی تولید و خواندن State ها را ندارند و تنها از read scope موجود در parent شان استفاده میکنند . هنگامی که یک state تغییر میکند آن ها recomposition را به نزدیک ترین scope ممکن منتقل می کنند. این نکته باعث افزایش پرفرومنس ران تایم می شود اما همچنان می تواند باعث ایجاد مشکلاتی برای ما بشود  (در انتهای مقاله در رابطه با این موارد بیش تر صحبت می کنیم ) اجازه دهید تا با ذکر یک مثال به بیان یک مشکل بپردازیم :@Composable
fun StateScreen() {
    var count by remember { mutableStateOf(0) }

    Column {&#039;count: $count&#039;)
        Button( = { count++ }) {
            Text(text = &#039;count++&#039;)
        }
    }
}کد بالا را در نظر بگیرید هنگامی که روی باتن کلیک میشود مقدار count زیاد میشود . مقدار count در یک Text و در Button نیز نمایش داده میشود .به دقت به کد بالا دقت کنید آیا در آن مشکلی میبینید؟  هنگامی که روی باتن کلیک می شود و مقدار count تغییر می کند Text و Button باید ریکامپوز شوند زیرا state آن ها (count) تغییر داشته است  . اما مشکل این جا است که اگر ریکامپوزیشن های این تابع را بررسی کنیم متوجه میشویم که خود StateScreen نیز با هر بار کلیک روی باتن ریکامپوز می شود (در حالی که state آن تغییری نداشته است ) به طور مثال اگر من 15 بار روی باتن کلیک کنم فانکشن StateScreen نیز 15 بار ریکامپوز میشود.مشکل از کجاست ؟ مگر smart recomposition نباید قسمت هایی که احتیاج به تغییر ندارند را skip کند؟منبع این مشکل را می توان در column جستجو کرد . همانطور که قبلا گفته شد کامپوزبل column به صورت inline مارک شده است . این سبب میشود تا کامپوزبل readScope خود را از دست بدهد . به این معنا که توانایی تولید و پردازش state ها را ندارد و تغییرات state را به نزدیک ترین readScope به خود منتقل می کند .این امر باعث می شود تا در مثال ما تغییرات state از column به نزدیک ترین scope ممکن که StateScreen است نشت کند و در نتیجه منجر به  ریکامپوز های بی مورد در آن شود .اما این مشکل قابل کنترل است :‌ چه می شود اگر column را در یک composable function دیگر wrapp کنیم ؟ به مثال زیر توجه کنید .@Composable
fun StateScreen() {
    var count by remember { mutableStateOf(0) }

    WrappedColumn {
        Text(text = &#039;count: $count&#039;)
        Button( = { count++ }) {
            Text(text = &#039;increase count&#039;)
        }
    }
}
@Composable
fun WrappedColumn(content: @Composable ColumnScope.() -&gt; Unit) {
    Column(content = content)
}در مثال بالا column در یک تابع composable دیگر wrapp شده است این باعث میشود تا این composable function نقش readScope را برای column بازی کند .در نتیجه state از WrappedColumn به StateScreen  نشت نمی کند .اکنون اگر من 15 بار روی button کلیک کنم StateScreen به هیچ عنوان ریکامپوز نمی شود ..ما به انتهای این مقاله رسیدیم . حالا وقتشه که compose metrics رو در پروژه ی خودتون فعال کنید و کامپوزبل های خودتون رو بررسی کنید . ممنون که تا پایان با من همراه بودید . </description>
                <category>Mohsen Abedini</category>
                <author>Mohsen Abedini</author>
                <pubDate>Thu, 17 Aug 2023 19:06:18 +0330</pubDate>
            </item>
                    <item>
                <title>در اعماق گریدل (gradle)</title>
                <link>https://virgool.io/codenevis/gradle-in-depth-fqd7hgykmzyf</link>
                <description>به عنوان یه برنامه نویس اندروید قطعا از گریدل استفاده کردید و بهش ارادت خاصی دارید . تو این مقاله هدفمون اینه که یه قدم فراتر بریم و ببینیم که گریدل اصلا چیه ، چه طور کار میکنه ، از چه مکانیسم هایی برای کش استفاده می کنه ، آیا میتونیم سرعت گریدل رو بالا تر ببریم یا به طور مادرزادی کنده و در نهایت اینکه چه طور می تونیم برای گریدل تسک بنویسیم و کلی چیز دیگه ...اما قبل از اینکه شروع کنیم به یه سوال مهم جواب میدیم :به عنوان توسعه دهنده اندروید اصلا چرا باید با سازوکار گریدل آشنا باشیم ؟ درسته که گریدل خودش همه ی کار هارو انجام میده و ما معمولا در حد اضافه کردن یه سری پلاگین و دیپندنسی باهاش کار می کنیم ، اما همون طور که میدونید بخش قابل توجهی از زمان ما هم برای فیکس یا بیلد گریدل از دست میره .آشنایی با نحوه کار گریدل شاید بتونه به ما کمک کنه تا زمان هایی که با ارور های گریدل درگیر هستیم کاهش پیدا کنه و زودتر بتونیم علت خطا رو پیدا کنیم .یا اینکه بتونیم با اقداماتی سرعت بیلد گریدلمون رو افزایش بدیم .یا حتی شاید در شرایطی قرار بگیریم که لازم باشه به گریدل تسک های کاستوم خودمون رو اضافه بکنیم. (مثلا در مواردی که در حال توسعه ی یک لایبرری یا  تعریف یک قرارداد برای پروژه ی خود هستیم )اگر شما هم مثل من از اون دسته آدم هایی هستید که دوست دارید صرفا یه استفاده کننده نباشید و از بکگراند همه چیز سر در بیارید  تا انتهای این مقاله همراه من باشید .گریدل چیست ؟طبق گفته ی داکیومنتش :گریدل یه build automation tool اپن سورسه که طوری طراحی شده تا با انواع مختلفی از نرم افزار ها سازگار باشه . Gradle در مورد اونچه که می خواهیم بسازیم یا چگونگی ساختنش، فرضیات کمی داره. و همین باعث می شه که Gradle انعطاف پذیری بالایی داشته باشه.اما حالا  build automation tool ها چه ابزار هایی هستن؟ابزار هایی هستن که فرایند تولید یک نرم افزار (به طور مثال شامل کامپایل کد ها ، ساختن پکیج ها و اجرای تست ها) را به صورت اتوماسیون شده انجام می دهند.از برخی از این ابزار ها می توان به Make, Rake, CMake, MSBuild, Ant, Maven , ... اشاره کرد.مفاهیم پایه (basic concepts) :در این جا با برخی از اصطلاحات و موارد پایه ای گریدل آشنا میشیم :پروژه :  یک پروژه اصطلاحا چیز هایی است که گریدل آن هارا بیلد می کند . هر پروژه شامل یک فایل build script است که معمولا در دایرکتوری ریشه ی پروژه نگه داری می شود و معمولا به نام های build.gradle یا build.gradle.kts قابل مشاهده است .این فایل شامل تعاریف تسک ها ، دیپندنسی ها ، پلاگین ها و سایر کانفیگ های پروژه است .تسک : تسک ها یا وظایف گریدل شامل لاجیک کارهایی است که گریدل باید انجام دهد تا بتواند بیلد بگیرد ، تست هارا اجرا کند و یا فرایند هایی که بتواند نرم افزار را دیپلوی کند.در گریدل تسک های بسیاری به صورت پیشفرض وجود دارد اما در برخی از موارد شرایطی پیش می آید که بخواهیم تسکی را اضافه کنیم یا پیاده سازی تسک های موجود را تغییر دهیم و یا آن ها را بسط دهیم .در گریدل هر تسک شامل 3 مورد است * :1. اکشن (Actions) : اکشن قسمتی است که کار و عملی را انجام می دهد -&gt; به طور مثال فرایند کامپایل کردن یک کد 2. ورودی (Inputs) : ورودی ها شامل فایل ها ، مقادیر و یا دایرکتوری هایی است که اکشن ها از آن ها استفاده می کنند و یا روی آن ها عملی انجام می دهند -&gt; به طور مثال کد های نوشته شده توسط برنامه نویس که قرار است کامپایل شوند.3. خروجی (Outputs) : فایل ها و یا دایرکتوری هایی که اکشن ها آن ها را ایجاد کرده اند و یا تغییری روی آن اعمال کرده اند -&gt; به طور مثال کد کامپایل شده که توسط اکشن تولید شده است .از این مفاهیم در آینده برای توضیح مکانیسم های کش گریدل و ایجاد تسک ها استفاده می کنیم ، برای درک بهتر آن می توانید یک فانکشن در برنامه نویسی را تصور کنید که ورودی هایی را دریافت کرده و روی آن ها تغییراتی اعمال می کند و مقدار تغییر یافته را به خروجی ارسال می کند . (البته که مانند یک تابع وجود ورودی و خروجی همیشه الزامی نیست .)فایل gradle-wrapper : یک اسکریپت اجرایی است که یک نسخه ی خاص از گریدل در آن تعریف شده است و هنگام اجرا بررسی میکند که ایا گریدل بر روی سیستم جاری نصب شده است یا خیر . در صورت نصب نبودن آن را دانلود کرده و نصب می کند . به این ترتیب از انجام دستی آن توسط دولوپر جلوگیری می کند .نکته : در آینده خواهیم دید که برای اجرای دستورات گریدل از طریق کامند لاین به دو طریق می توانیم عمل کنیم :1) استفاده از کامند gradle به طور مثال (gradle build)2) استفاده از کامند gradlew به طور مثال (gradlew build)اما تفاوت این دو در چیست ؟ هر دو یک خروجی را ایجاد می کنند اما تفاوت آن ها در نحوه ی اجراست اگر با کامند gradle دستورات را اجرا کنید گردیل باید حتما روی سیستم شما نصب باشد و path آن به سیستم معرفی شده باشد ، اما اگر از کامند gradlew استفاده کنید اجرای دستور از کانال gradle-wrapper عبور می کند ، به این معنا که اگر گریدل روی سیستم شما نصب نباشد ابتدا فایل wrapper را اجرا کرده و آن را نصب و کانفیگ می کند ، سپس دستور شما را اجرا خواهد کرد (از fail شدن دستور به دلیل نبود gradle جلوگیری می کند.)پلاگین ها :  قطعا تا کنون پیش آمده است که پلاگین هایی را به گریدل خود اضافه کنید مثلا در مواردی که پلاگین های dagger را به پروژه خود اضافه کردید .pluginsپلاگین ها به شما این امکان را می دهند تا بتوانید تسک ها و کانفیگ ها را فراتر از آن چه در گریدل (به صورت پیشفرض) تعریف شده است به پروژه ی خود اضافه کنید .به کمک پلاگین ها می توانید یک تسک یا کانفیگ را یکبار ایجاد کرده و در سورس های مختلفی از آن استفاده کنید . (درست مانند یک لایبرری که بعد از تعریف دیپندنسی آن ، کد هایش به پروژه ی شما اضافه می شود)مثلا هنگامی که پلاگین dagger را در پروژه تعریف می کنید ، این پلاگین تسک های مورد نیاز dagger را به گریدل شما اضافه می کند و به کمک این تسک ها می تواند کلاس های مورد نیاز را جنریت کند .فاز های بیلد (Build phases) : گریدل دارای lifecycle مشخصی است که شامل 3 فاز است * :1. فاز Initialization -&gt;  محیط بیلد را ایجاد می کند و کانفیگ های لازم را بر اساس فایل settings.gradle اعمال می کند . 2. فاز Configuration -&gt; در این مرحله  build.gradle  خوانده و تفسیر می شود سپس گریدل گراف تسک ها را ایجاد می کند و آن را پیکر بندی می کند. گراف تسک ها *: این گراف  مانند یک درخت برعکس است که ریشه در بالا ترین قسمت آن قرار دارد و مشخص می کند که کدام یک از تسک ها و با چه ترتیبی باید اجرا شوند .دو تسک A و B را در نظر بگیرید که خروجی تسک A ورودی تسک B است (B به A وابستگی دارد) در این صورت تسک A حتما باید قبل از B اجرا شود ، گریدل این ترتیب را در گراف تسک ها مشخص می سازد .3. فاز Execution -&gt; در این فاز که همان فاز اجرایی است ، گریدل بر اساس گراف تسک ها ، آن ها را اجرا می کند .بیش تر زمان در گریدل صرف تسک های اول و دوم می شود ، ایجاد گراف تسک ها یک عمل پیچیده است و کوچکترین خطا در آن می تواند تمام گراف را بی اعتبار کند و مشکلات جدی ایجاد کند.بیلد (Build) : بیلد در گردیل شامل اجرای مجموعه ای از تسک ها است که توسط command line یا ide ها اجرا می شود . گریدل همواره سعی می کند تا وظایف اصلی خود را با اجرای کم ترین تسک های ممکن به پایان برساند گریدل دیمون (Gradle Daemon)  : شاید متوجه شده باشید که در برخی از موارد با اینکه استفاده ی خاصی از اندروید استادیو ندارید (صرفا در حال کد زدن هستید) اما همچنان مصرف ریسورس ها توسط ide بالاستاما چرا ؟ در این جا به تعریف دیمون می پردازیم :دیمون ها فرایند های پردازشی طولانی مدتی هستند که همواره در بکگراند اجرا می شوند و به کمک آن ها می توان build time را کاهش داد .به بیان ساده تر گریدل برخی از وظایف خودش رو به جای اینکه در زمان بیلد انجام بده در زمان هایی که مشغول بیلد نیستیم انجام میده و باعث میشه تا زمان بیلد به طور قابل توجهی کاهش پیدا کنه .برخی از این وظایف شامل موارد زیر است :1.  کش کردن اطلاعات و وضعیت پروژه که در حین بیلد تولید شده است .2. فعال نگه داشتن Jvm ، بنابراین در هر بیلد نیاز به انتظار برای فعال شدن jvm ندارید .3. بهینه سازی های مداوم زمان اجرا در jvm .4. بررسی فایل سیستم برای محاسبه دقیق آنچه نیاز دارد قبل از اجرای یک بیلد بازسازی شود.نکته : دیمون به صورت پیشفرض فعال است اما اگر در مواردی که با کمبود ریسورس ها به خصوص ram مواجه هستید (به طور مثال در سرور های CI) میتوانید آن را غیر فعال کنید (البته که انجام این کار زمان بیلد را افزایش می دهد و توصیه شده نیست)برای غیر فعال سازی آن به فایل gradle.properties مراجعه کنید و خط زیر را به آن اضافه کنید:org.gradle.daemon=falseکامند های گریدل (Gradle CLI) :  برای اجرای تسک های اجرایی گریدل می توانید علاوه بر Ide از کامند های گریدل نیز استفاده کنید .ساختار کامند ها به شکل روبرو است :gradle/gradlew + taskNameبه طور مثال برای بیلد گرفتن از پروژه در اندروید استادیو دوبار بر روی CTRL کیبورد کلیک کنید و در کادر باز شده کامند بیلد (gradle assembleDebug) را وارد کنید و ENTER بزنید آنگاه تسک بیلد شروع به اجرا می کند. همچنین در کادر باز شده سایر کامند ها به همراه توضیحات آن ها موجود است .When you understand how to move from using Gradle via all the green buttons, to issuing actual CLI commands, you become the real Android developer  ??مکانیسم های کش (cache) در گریدل :حالا بریم سراغ یکی از قسمت های مهم گریدل ? مکانیسم های کش کمک میکنن تا بتونیم زمان زیادی رو به وسیله ی استفاده ی مجدد از output های تولید شده در بیلد های قبلی save کنیم . قطعا متوجه این شده اید که اولین بیلد معمولا زمان زیادی طول میکشه اما در بیلد های بعدی این زمان به شدت کاهش پیدا میکنه (تسک های بدون تغییر اجرا نمی شوند و لیبل UP_TO_DATE می گیرد)، این به دلیل همان کش های صورت گرفته در گریدل است که اگر وجود نداشت قطعا تمام بیلد های شما به همان اندازه ی اولین بیلد پروژه طول می کشید مکانیسم های کش output های تولید شده از تسک ها را به صورت لوکال (روی سیستم کاربر) یا ریموت (روی یک سرور مرکزی) ذخیره میکند و به تسک های بعدی اجازه می دهد تا از این خروجی ها استفاده کنند البته به شرطی که تشخیص دهد input ها تغییری نداشته اند .در نوشتن تسک های کاستوم باید ورودی و خروجی های تسک را آنقدر واضح تعیین کنیم تا گریدل بتواند خروجی تسک ما را کش کند و همچنین با انجام برخی از اقدامات گریدل را متوجه می کنیم تا از اجرا های بی مورد تسک ما پرهیز کند.حالا با برخی از این مکانیسم های کش آشنا میشیم :M) incremental buildsقطعا همه ی ما نتایج بیلد های افزایشی را دیده ایم . محل قرار گیری نتایج این نوع از بیلد در پروژه ، همان دایرکتوری build است که کد های جنریت شده قرار می گیرند به طور مثال کد هایی که توسط Dagger یا Binding جنریت می شوند .نکته : به کمک دستور gradlew assembleDebug در پروژه های اندرویدی می توان نتایج  این بیلد را ایجاد کرد و به کمک دستور gradlew clean می توان این نتایج را پاک کرد .اولین قدم برای سرعت بخشیدن به بیلد های ما، به حداکثر رساندن استفاده مجدد از آنچه Gradle در این دایرکتوری ها قرار می دهد است (به شرطی که محتوای فایل های جنریت شده تغییر نکرده باشد) . Gradle این استفاده مجدد را &quot;incremental building&quot; می نامد.A) build cacheکش قبلی (incremental) تنها روی پروژه ی جاری بر روی سیستم شما اعمال می شود اما در این نوع از بیلد شما میتوانید output هارا بین پروژه های مختلف روی یک سیستم و یا حتی روی یک ماشین دیگر (به طور مثال بر روی یک سرور مرکزی) تولید کنید و سایر دولوپر ها صرفا از این خروجی ها استفاده کنند.این یه امر منطقیه تصور کنید یک پروژه ی مثلا اندرویدی به نام A و یه پروژه ی دیگه به نام B روی لب تابتون دارید ، کش های حاصل از بیلد افزایشی A , B را نمی توانید برای هم دیگر استفاده کنید چون فایل های جنریت شده برای هر پروژه مجزا است ، اما موارد کش شده توسط build cache در تمام پروژه های شما (A , B , ...) قابل استفاده است .در این نوع از بیلد ها هم مشابه بیلد افزایشی در صورت یکسان بودن ورودی و به تناسب آن خروجی ، گریدل تسک را مجددا اجرا نمی کند و صرفا از خروجی بیلد قبلی استفاده میکند ، با این تفاوت که برخلاف بیلد افزایشی که خروجی را صرفا از دایرکتوری build فراخوانی می کرد و صرفا روی یک ماشین کار می کرد می تواند خروجی ها را از هر بیلدی در هر کجای ماشین شما یا ماشین های دیگر فراخوانی کند.* این نوع از کش به طور پیشفرض روی گریدل فعال نیست و برای فعال سازی آن باید لاین زیر را به gradle.properties اضافه کنید:org.gradle.caching=trueH) Android’s build cacheدو بیلد قبلی در هر گریدل پروژه ای موجود بود ، اما این نوع از بیلد صرفا مخصوص پروژه های اندرویدی است که توسط Android Gradle Plugin (AGP) تعدادی مکانیسم کش جدید به پروژه های اندرویدی اضافه می کند .بیلد کش (build cache) ای که در مورد قبلی معرفی شد برای پیچیدگی های یک پروژه ی اندرویدی کافی نبود به همین دلیل بود که android build cache معرفی شد.وظیفه ی این بیلد کش ذخیره ی outputs های خاصی است که توسط android plugin برای گریدل هنگام بیلد پروژه ی ما تولید می کند است . (مانند AARs)کش های اندروید همچنین روی سرورهای یکپارچه سازی پیوسته (CI) و هنگام اجرای چندین پروژه بر روی یک ماشین محلی  در دسترس است.نکته: این کش ها در دایرکتوری users/home/.android/.build_cache/ ذخیره می شوند (البته محل های احتمالی دیگری برای ذخیره ی آن ها وجود دارد ) و می توان با اجرای دستور gradlew cleanBuildCache آن ها را پاک کرد.S) Memory cache with the Daemonدر هر یک از بیلد قبلی‌مان، ما یک کش دیگر داشتیم که به نفع ما کار می‌کرد که قبلا در رابطه با آن صحبت کرده ایم . این ذخیره سازی توسط Gradle Daemon ارائه شده است.مزیت آشکار دیمون این است که Gradle فقط یک بار برای چندین بیلد در حافظه بارگذاری می شود، نه یک بار به ازای هر بیلد.کد به تدریج در طول اجرا بهینه می شود، به این معنی که ساخت های بعدی می توانند به دلیل این فرآیند بهینه سازی سریعتر باشند.Daemon همچنین اجازه می دهد تا کارایی بیشتری در کش کردن حافظه در سراسر بیلدها داشته باشد. برای مثال، کلاس‌های مورد نیاز بیلد (مانند پلاگین‌ها، اسکریپت‌های ساخت - build scripts) را می‌توان در حافظه بین بیلد ها  نگه داشت.A) Third-party dependency cachingحافظه نهان دیگری که Gradle ارائه می دهد، کش کردن دیپندنسی های شخص ثالث است. اکثر برنامه های اندروید حداقل به چند دیپندنسی شخص ثالث وابسته هستند. حتی اگر فقط از کدهای AndroidX libs استفاده می کنید، باز هم دیپندنسی هایی دارید که باید از مخازن مرکزی دانلود شوند. Gradle این وابستگی ها را در Dependency Cache با نام مناسب ذخیره می کند.درست مانند build cache، این کش در تمام پروژه ها در یک دستگاه موجود است. (به همین دلیل است که اگر دو پروژه اندرویدی با دیپندسی هایی که ورژن مشابه دارند روی یک سیستم ایجاد کنید ، دیپندنسی ها را یکبار دانلود می کند و در پروژه ی دوم از همان ها استفاده میکند)گریدل این کش ها را در GRADLE_HOME/.gradle/caches ذخیره می کند .بهینه سازی بیلد تایم در گریدل تاکنون از بخش های قبلی با سه عامل موثر در بهینه سازی بیلد تایم گریدل آشنا شدیم :1) بهینه سازی به کمک کش کردن خروجی های هر تسک 2) بهینه سازی به کمک کش کردن وابستگی ها و لایبرری ها 3) بهینه سازی به کمک اجرای daemonاکنون به عوامل دیگری که می توانند به افزایش سرعت بیلد کمک کنند 4) configuration cache (experimental)همانطور که در بخش فاز های گردیل صحبت کردیم بیش تر زمان گریدل صرف فاز های Initialization و Configuration می شود . تاکنون تمام مکانیسم های کش که در رابطه با آن ها صحبت کردیم در فاز Execution بودند . اما برای افزایش حداکثری سرعت بیلد احتیاج داریم تا مکانیسم هایی برای کش در فاز های اول و دوم پیاده سازی کنیم . اما بهینه سازی در این دو مرحله به هیچ وجه کار آسان و بی خطری نیست ، کوچکترین خطا در این دو مرحله می تواند باگ های فراوان و غیر قابل کشف ایجاد کند .اما اکنون تیم توسعه دهنده ی گریدل توانسته فیچری آزمایشی به جهت کش در مرحله ی Configuration ارایه دهد :کانفیگ کش مکانیسمی است که به کمک آن می توانیم گراف تسک های تولید شده توسط گریدل را که قبلا پیرامون آن صحبت کردیم کش کنیم . در حالت پیشفرض هر بار که بیلد می گیرید گریدل گراف تسک ها را مجددا تولید کرده و از آن استفاده می کند اما به کمک این قابلیت گریدل فقط زمانی گراف را مجددا ایجاد می کند که متوجه شود تغییری در آن رخ داده است.این قابلیت هم اکنون به صورت پیشفرض در گریدل فعال نیست و تیم گریدل همچنان در حال توسعه ی آن است (experimental) برای فعال سازی این نوع از کش خط زیر را به gradle.properties اضافه کنید:org.gradle.unsafe.configuration-cache=true5) parallel executionدر اکثر پروژه ها ، زیر پروژه (subproject) هایی وجود دارد (مانند معماری ماژولار در پروژه های اندرویدی) که در وضعیت اشتراک ریسورس ها یا وابستگی نیستند .یعنی ماژول ها از هم مستقل هستند و به یکدیگر دیپندنسی ندارند ، در چنین شرایطی گریدل می تواند تسک هارا به صورت موازی جلو ببرد بدون آنکه مشکلی در سایر قسمت ها ایجاد شود.به صورت پیشفرض گریدل فقط قابلیت اجرای یک تسک در آن واحد را دارد اما با افزودن خط زیر به gradle.properies گریدل از قابلیت پردازش موازی بهره مند می شود.org.gradle.parallel=trueتاثیر پردازش موازی بر زمان بیلد پروژه وابسته به معماری پروژه و میزان وابستگی های آن است.6) Re-enable the Gradle Daemonما در بخش های قبل به طور مفصل در رابطه با دیمون صحبت کردیم .دیمون  به صورت پیشفرض روی گریدل فعال است.اما در برخی از شرایط بیلد ما ممکن است دیمون را غیر فعال کند، برای جلوگیری از این اتفاق می توانیم آن را override کنیم به این منظور در فایل gradle.properties لاین زیر را اضافه می کنیم :org.gradle.daemon=true7) Increase the heap sizeگریدل به صورت پیشفرض 512 مگابایت از فضای heap را برای بیلد ما اختصاص می دهد ، این مقدار در اکثر پروژه ها کافی است ، اما اگر بیلد ها سنگین هستند و به فضای بیش تری احتیاج دارند این مقدار را به کمک افزودن خط زیر به gradle.properties می توان افزایش دادorg.gradle.jvmargs=-Xmx2048M8) Optimize repository orderاین یک نکته ی جالب در گریدل است ?شما با تغییر ترتیب مخازن خود می توانید سرعت بیلد خودتون رو افزایش بدید. به کد زیر توجه کنید repositories {
    mavenLocal()
    mavenCentral()
    google()
    // other repositories go here
}هنگامی که گریدل در حال پیدا کردن دیپندنسی ها است به صورت پیشفرض آن ها را در مخازن تعریف شده از بالا به پایین جستجو می کند.بنابراین اگر بدانید دیپندنسی های شما بیش تر در کدام مخازن واقع شده اند با تغییر اولیت آن می توانید سرعت گریدل رو بالا ببرید . پیشنهاد می شود که ترتیب مخازن را به صورت زیر حفظ کنید:مخازن محلی: مخازنی که در سیستم شما نصب شده‌اند و شامل وابستگی‌هایی هستند که شما خودتان تولید کرده‌اید.مخازن (jcenter): jcenter یک مخزن مرکزی برای وابستگی‌های مختلف است که به صورت پیشفرض در Gradle تنظیم شده است. این مخزن شامل بیش از ۲۰۰ هزار وابستگی است و برای پیدا کردن بیشتر وابستگی‌ها اولین مخزنی است که باید در ترتیب مخازن در Gradle تعریف شود.مخازن مربوط به گوگل (Google): این مخازن شامل وابستگی‌هایی هستند که توسط گوگل ارائه شده‌اند، از جمله کتابخانه‌های مربوط به پشتیبانی از خدمات گوگل، مانند Google Play Services و Firebase.مخازن مربوط به سایر توسعه دهندگان: مخازنی که توسط توسعه‌دهندگان مختلف برای ارائه وابستگی‌های خود استفاده می‌کنند، مانند مخازن برای وابستگی‌های مربوط به کتابخانه‌های متن‌باز.* البته نکته ای که مطرح است گوگل به تازگی اعلام کرده که بزودی jcenter را خاموش می کند ، دلیل آن هم مسایل امنیتی به وجود آمده و آلوده شدن این ریپازیتوری است . همچنین توصیه کرده است تا به جای jcenter از maven central استفاده شود.* همچنین نکته ی مهم دیگر این است که برای افزایش سرعت گریدل تا جای ممکن تعداد مخازن خود را اندک نگه دارید.* و به عنوان نکته ی مهم آخر ، قاعده ی گفته شده برای مخازن پلاگین های گریدل نیز صادق است . تقریبا تمام پلاگین هایی که در اندروید استفاده می شود در دو مخزن Google و Maven Central موجود است اما اگر به هر دلیل به مخزن شخص ثالث gradlePortalPlugin احتیاج داشتید آن را در آخر بلاک ریپازیتوری و پایین تر از دو مخزن دیگر تعریف کنید .9) Use static dependency versionsاز راه های دیگر افزایش سرعت گریدل حذف داینامیک ورژن ها ( &quot; 2.+ &quot; ) است . داینامیک ورژن ها باعث می شوند تا گریدل دایما با مخازن در ارتباط باشد تا بتواند جدید ترین ورژن ها را پیدا کند.گریدل به طور پیشفرض یکبار در هر 24 ساعت با مخازن ارتباط برقرار می کند تا از اخرین تغییرات مطلع شود.10) JVM parallel garbage collectorیکی از مواردی که می تواند به سرعت اجرای اپ و گریدل شما کمک کند استفاده از parallel GC است .هر بار که زباله جمع کن فعال می شود می تواند هزینه ی قابل توجهی مانند بلاک شدن لحظه ای Main Thread را به پروژه شما اعمل کند .به منظور کاهش مدت زمان اجرای GC می توانید کد زیر را به گریدل اضافه کنید : org.gradle.jvmargs=-XX:+UseParallelGCتا این جای کار با مهم ترین روش های بهینه سازی گریدل آشنا شدیم ، البته بحث به این جا ختم نمی شود اگر مایلید تا با موارد بیش تری از جمله بهینه سازی های زمان اجرا در تست ها اشنا بشید می توانید این لینک را بررسی کنید. با مهم ترین موارد تئوری گریدل آشنا شدیم ، اکنون می خواهیم تا دست به کد شویم و برای گریدل تعدادی تسک بنویسیم .در نوشتن تسک ها از زبان کاتلین استفاده می کنیم زیرا سینتکس groovy  برای عمده ی ما نا آشناست و همچنین سرعت اجرای تسک های نوشته شده به زبان کاتلین و جاوا بیش تره .برای اینکه بتونیم از سینتکس کاتلین در گریدل استفاده کنیم نیاز داریم که بیاد گریدل هامون رو به فرم kts بنویسیم اگر تا حالا تجربه ی kts کردن گریدلتون رو نداشتید این لینک رو دنبال کنید (همچنین با این کار می توانید امکانات جالبی را به گریدل خودتان اضافه کنید) برای این قسمت لازمه تا به طور کامل با مفاهیم فاز های گریدل آشنا باشید . تسک نویسی (Authoring Tasks)ابتدا با یک تسک ساده شروع میکنیم . محلی که در آن تسک ها تعریف می شوند همان فایل build.gradle.kts  است .task(&#039;printHello1&#039;) {
    println(&#039;hello world1&#039;)
}برای تعریف این تسک از (&quot;name&quot;)task استفاده کرده ایم سپس در بلاک آن تابع println از کاتلین را فراخوانی کرده ایم . برای اجرای این تسک می توانید با دوبار کلیک روی کلید CTRL در ide های شرکت intelij و نوشتن دستور زیر ، تسک را اجرا کنید gradle printHello1 //windows
./gradle printHello1 //linuxدر دستور بالا printHello همان نامی است که برای این تسک تعریف کرده ایم . در واقع نام هر تسک شناسه ای برای اجرای آن تسک است .خروجی این تسک به این صورت در کنسول نمایش داده می شود :برای تعریف تسک ها در گریدل دو روش وجود دارد :1) تعریف تسک به صورت (&quot;name&quot;)task که در بالا مثالی از آن دیدیم 2) تعریف تسک به صورت (&quot;name&quot;)tsaks.register که پایین تر مثال هایی از آن می بینیم اما تفاوت این روش ها در چیست : روش اول هنگامی استفاده می شود که تسک ما یک تسک کوچک است همانند همین تسک hello worldاما روش دوم در تسک های پیچیده و بزرگ استفاده می شود .زیرا روش دوم از مکانیسم lazy evaluation استفاده می کند به این معنا که تسک فقط زمانی ساخته می شود که به آن نیاز باشد اما در روش اول به محض خوانده شدن build.gradle تسک نیز ساخته می شود .بنابراین به طور کلی ما از روش دوم برای تعریف تسک ها استفاده می کنیم .مثال hello world با روش رجیستر : tasks.register(&#039;printHello2&#039;) {
    println(&#039;hello world2&#039;)
}تا این جا با نحوه ی تعریف تسک ها آشنا شدیم حالا میریم سراغ مورد بعدی :doFirst{} and doLast{} methods : فرض کنید دو تسک زیر را در گریدل تعریف کرده ایم :tasks.register(&#039;printHello1&#039;) {   
  println(&#039;hello world1&#039;)
}


tasks.register(&#039;printHello2&#039;) {    
 println(&#039;hello world2&#039;)
}حالا تسک printHello2 را اجرا می کنیم ، انتظار داریم تا عبارت &quot;hello world2&quot; چاپ شود !عبارت چاپ می شود اما یک مشکلی به وجود می آید عبارت &quot;hello world1&quot; هم چاپ می شود اگر تسک 1 را هم اجرا کنیم هلو ورد 2 نیز چاپ میشود .ما فقط  یک تسک را اجرا کرده ایم اما چرا تسک دیگر نیز ران می شود ؟اصولا نوشتن تسک ها به فرم بالا یک اشتباه هست ما باید دستور println را که اکشن تسک هایمان است را در بلاک های doFirst یا doLast بنویسیم مانند کد زیر:tasks.register(&#039;printHello1&#039;) {
    doFirst{
        println(&#039;hello world1&#039;)   
    }
}


tasks.register(&#039;printHello2&#039;) {
    doFirst{
        println(&#039;hello world2&#039;)   
    }
}اما چرا ؟هنگامی که تسک ها را بدون بلاک های doFirst یا doLast بنویسیم اجرای تسک ها در فاز Configuration اتفاق می افتد (یعنی به محض جنریت شدن گراف ، تسک ران می شود .)همانطور که مطلع هستید هر تسکی که در گریدل اجرا می کنید به ازای آن تسک یکبار دیگر تمام build script ها خوانده می شود (فاز Initialization ) و مجددا گراف تسک ها از ابتدا ایجاد می شود (فاز Configuration) تنها فاز Execution  است که به ازای هر تسک به صورت اختصاصی ران می شود .بنابراین هنگامی که دستور println در فاز کانفیگ اجرا می شود به ازای تمام تسک ها ران می شود(حتی اگر تسک دیگری ران شده باشد).بنابراین هر گاه تسکی می نویسیم اکشن آن را داخل بلاک های doFirst و doLast می نویسیم تا لایف سایکل اکشن تسک (به طور مثال println) به فاز Execution  منتقل شود (این باعث می شود تا اکشن تسک فقط زمانی اجرا شود که تسک آن ران شده باشد. )تفاوت doFirst با doLast چیست ؟ بلاک doFirst اولین کار هایی که در حین اجرای یک تسک باید انجام شود را مشخص می کند .بلاک doLast اخرین کار هایی که در حین اجرای یک تسک باید انجام شود را مشخص می کند .در واقع این دو متد برای مشخص کردن اولویت اجرا وظایف در یک تسک به کار می روند ، به طور مثال در یک تسک می خواهیم کار A حتما قبل از B انجام شود پس به این صورت کد آن را می نویسیم :task.register(&#039;name&#039;){
     doFirst{ A }
     doLast{ B }
}اما در مثال println که بالاتر آوردیم تفاوتی در استفاده ی doFirst  یا doLast مطرح نیست از هر کدام که مایل باشیم می توانیم استفاده کنیم .تاکنون موفق شدیم یک تسک ساده بنویسیم و با مفاهیم doFirst , doLast آشنا بشیم ، اکنون برای جمع بندی این قسمت یک تسک پیچیده تر برای گریدل می نویسیم:کار کردن با فایل ها در گریدل یکی از تسک های بسیار مرسوم است ، اکنون میخواهیم یک تسک بنویسیم تا در روت پروژه یک دایرکتوری به نام generate ایجاد کند (اگر وجود نداشت) و در این دایرکتوری یک فایل با نام log.txt ایجاد کند سپس داخل فایل txt عبارت version = 1.0.0 را بنویسد . کد این تسک به صورت زیر است :task(&#039;createLogFile&#039;) {
    doLast {
        val targetDir = layout.projectDirectory.dir(&#039;/generate&#039;).toString()   1️⃣
        File&#40;targetDir&#41;.mkdirs()   2️⃣
        File&#40;targetDir, &#039;log.txt&#039;&#41;.writeText(&#039;version = 1.0.0&#039;)  3️⃣
    }
}1️⃣ : در قسمت اول می خواهیم تا محل قرار گیری پروژه را پیدا کنیم (این محل در سیستم های مختلف متفاوت است) به کمک layout.projectDirectory محل قرار گیری پروژه را پیدا می کنیم .خروجی این دستور در سیستم من به این صورت است : C:\Users\mohsen\IdeaProjects\GradleCustomTask به طور کلی هر گاه به دنبال آدرس پروژه ی خودمان بودیم از ابزار layout استفاده می کنیم .اکنون به کمک متد dir دایرکتوری generate را به مسیر اضافه کرده ایم (به علامت /  دقت کنید)و در نهایت کل این مسیر را به یک رشته تبدیل کرده ایم.2️⃣ : در این قسمت File را از پکیج java فراخوانی کرده ایم و مسیری را که در 1️⃣ ایجاد کرده بودیم را به عنوان آرگومان به آن داده ایم . سپس با کال کردن متد mkdirs دایرکتوری generate را ایجاد کرده ایم.3️⃣ : حالا در قسمت اخر مجددا File را فراخوانی کرده و محل قرار گیری فایل و اسم و فرمت آن را به عنوان آرگومان داده ایم این عبارت فایل log.txt را در مسیر دایرکتوری generate ایجاد می کند.در انتهای کار هم با متد writeText عبارتی را که میخواستیم در فایل Log نوشته ایم.تسک های پیشفرض (built In Tasks)تیم گریدل تسک های پر تکرار و کاربردی را به صورت پیشفرض در گریدل ایجاد کرده است که برای کار های مختلف می توانیم آن ها را بسط داده و از آن های استفاده کنیم . به عناون مثال می توان به تسک های Delete , Copy , Jar , Compress و ... اشاره کرد .در این مقاله با برخی از این تسک ها کار خواهیم کرد .تسک (Copy)فرض کنید که فایل log.txt که در تسک قبل ایجاد کردیم را میخواهیم در یک لوکیشن دیگر کپی کنیم و یا حتی بعد از کپی نام آن را عوض کنیم ، می توانیم با استفاده از File مجددا این کار را انجام دهیم ، اما به منظور راحتی کار از تسک Copy استفاده می کنیم . ساختار این تسک به شکل زیر است :task(&#039;copyLogFile&#039;, Copy::class) {    0️⃣
    val sourceDir = layout.projectDirectory.dir(&#039;/generate/log.txt&#039;).toString() 1️⃣
    val targetDir = layout.projectDirectory.dir(&#039;/src/main/resources&#039;).toString() 2️⃣
    from(sourceDir) 3️⃣
    into(targetDir) 4️⃣
    rename(&#039;log.txt&#039;, &#039;ConfLog.txt&#039;)  5️⃣
}0️⃣ : به نحوه ساخت این تسک توجه کنید برخلاف تسک های قبلی یک ارگومان جدید به نام Copy::class پاس داده شده است (این کلاس از پکیج gradle ایمپورت شده است). به این ترتیب در این تسک ما می توانیم از Api های تسک Copy نیز استفاده کنیم .1️⃣ : در قسمت یک محل قرار گیری فایل log.txt مشخص شده است . 2️⃣ : در قسمت دوم جایی که می خواهیم فایل در آن کپی شود را مشخص کرده ایم .3️⃣ : قسمت 3 به دلیل وجود تسک Copy در دسترس است . متد from محل قرار گیری فایل را دریافت می کند.4️⃣ : قسمت 4 به دلیل وجود تسک Copy در دسترس است . متد into محلی که میخواهیم فایل در آن کپی شود را دریافت می کند .تا این جای کار اگر تسک را اجرا کنیم فایل log.txt کپی می شود اما :5️⃣ : در قسمت 5 که به دلیل وجود تسک Copy در دسترس است نام فایلی که میخواهد کپی شود را از log.txt به ConfLog.txt تغییر دادیم .اکنون بعد از اجرا فایل با نام جدی کپی میشود. تسک (Delete)آیا تمایل دارید تا فایل log.txt را که از آن یک کپی گرفتیم را با یک تسک دیگر پاک کنیم . تسک زیر این کار را انجام می دهد :task(&#039;deleteLogFile&#039;, Delete::class) {
    val targetDir = layout.projectDirectory.dir(&#039;/generate/log.txt&#039;).toString()
    delete(targetDir)
} اکنون باید بدانید که این تسک چگونه کار می کند.تعریف وابستگی در تسک ها تسک های createLogFile و copyLogFile و deleteLogFile را که در قسمت قبل بررسی کردیم را به خاطر بیاورید . ما این تسک ها را استپ به استپ به صورت دستی اجرا می کردیم تا نتایج آن را مشاهده کنیم .اکنون می خواهیم کاری کنم تا وقتی تسک createLogFile انجام شد و فایل txt جنریت شد تسک copyLogFile به طور خودکار اجرا شود و از فایل کپی بگیرد ، سپس بعد از کپی شدن فایل، تسک deleteLogFile به طور خودکار اجرا شود و txt اصلی را پاک کند .قبلا در رابطه با گراف تسک ها صحبت کرده ایم برای انجام این کار باید در فاز کانفیگ در تسک ها وابستگی ایجاد کنیم تا در گراف ، تسک ها به صورت وابسته تعریف شوند .قبل از اینکه تسک های فایلمون رو وابسته کنیم انواع روش های تعریف وابستگی را توضیح می دهیم .وابستگی به کمک (depend on) مثال زیر را در نظر بگیرید :tasks.register(&#039;printHello1&#039;) {
    doFirst{
        println(&#039;hello world1&#039;)
    }
}


tasks.register(&#039;printHello2&#039;) {
    doFirst{
        println(&#039;hello world2&#039;)
    }
    dependsOn(&#039;printHello1&#039;)
}در این مثال ما اجرای تسک printHello2 را وابسته به اجرای printHello1 کرده ایم ، یعنی اگر تسک printHello2 را اجرا کنیم قبل از اجرای این تسک ، تسک printHello1 اجرا می شود . ما این کار را به کمک متد dependsOn انجام دادیم ، ورودی این متد نام یک تسک دیگر است.اکنون یک سوال کنکوری **** :چرا dependOn را خارج از بلاک doFirst تعریف کرده ایم ؟قبلا گفتیم که بلاک doFirst لایف سایکل را به فاز Execution منقل می کند . فاز Execution بعد از فاز کانفیگ انجام می شود یعنی جایی که گراف تسک ها دیگر ساخته شده است .ما دیپندسی را فقط در فاز کانفیگ می توانیم ایجاد کنیم یعنی جایی که گراف قرار است ساخته شود.اگر در حین اجرای یک تسک بخواهیم تغییری در گراف ایجاد کنیم به آن معناست که گراف invalid شده است *بنابراین حتی اجرای تسک جاری نیز زیر سوال می رود بنابراین گریدل ارور زیر را برای شما ایجاد می کند.وابستگی به کمک (finalizedBy) قبل از اینکه به این قسمت برسیم اجازه بدید تا همین جا یک نکته را توضیح بدهم:همونطور که قبلا مطرح کردیم گریدل شامل فاز های متفاوتی است این به آن معناست که گریدل شامل یک لایف سایکل مشخص است و مثل هر سیستم دیگری که لایف سایکل دارد می تواند callback های مختلفی داشته باشد .به طور مثال لیستنر های زیر را در نظر بگیرید که همگی در فایل build.gradle تعریف شده اند و متناسب با کار خود می توانیم از آن ها استفاده کنیم :gradle.taskGraph.whenReady {  }
gradle.taskGraph.afterTask{  }
gradle.taskGraph.addTaskExecutionGraphListener {  }
gradle.buildFinished {  }
gradle.projectsLoaded {  }اکنون کد زیر را برای تعریف دیپندنسی در نظر بگیرید : tasks.whenTaskAdded {  1️⃣
    if (this.name == &#039;printHello1&#039;) {  2️⃣
        this.finalizedBy(&#039;printHello2&#039;)  3️⃣
    }
}

tasks.register(&#039;printHello1&#039;) {
    doFirst{
        println(&#039;hello world1&#039;)
    }
}


tasks.register(&#039;printHello2&#039;) {
    doFirst{
        println(&#039;hello world2&#039;)
    }
}1️⃣ : در قسمت یک ما روی تسک ها لیستنر whenTaskAdded را ست کرده ایم ، این لیستنر هنگامی که هر تسک ساخته و add شد در فاز کانفیگ کال می شود .(توجه کنید که در فاز اجرا تعریف وابستگی غیر ممکن است.)2️⃣ : در قسمت دوم بررسی کرده ایم که آیا تسک ساخته شده همان تسک printHello1 هست یا خیر.3️⃣ : در قسمت سوم بیان کرده ایم که اگر تسک ما printHello1 بود انجام آن تسک را با اجرای تسک printHello2 به پایان برساند (درواقع بعد از اجرای موفقیت آمیز printHello1 ، تسک printHello2  اجرا می شود )این وابستگی به کمک یک finalizer اتفاق می افتد .کد بالا به منظور آشنایی شما با لیستنر ها مطرح شد اما آیا نمیشد به همان روشی که از depend استفاده کردیم از finalizer هم استفاده کنیم ؟ پاسخ بله است ?، به کد زیر دقت کنید :tasks.register(&#039;printHello1&#039;) {
    doFirst{
        println(&#039;hello world1&#039;)
    }
    finalizedBy(&#039;printHello2&#039;)
}


tasks.register(&#039;printHello2&#039;) {
    doFirst{
        println(&#039;hello world2&#039;)
    }
}تفاوت finalizedBy با depend on چیست ؟به عقب برگردید و کد ها را دوباره بررسی کنید حدس میزنید که تفاوت در چیست ؟فرض کنید که گریدل ما شامل تسک های A , B است :میخواهیم بگوییم که اگر تسک A انجام شد بعد از آن تسک B را هم انجام بده -&gt; این همان کاربرد finalizeBy است .میخواهیم بگوییم که اگر تسک B اجرا شد قبل از اجرای آن تسک A را ابتدا انجام بده -&gt; این همان کاربرد dependOn است .حالا مثال تسک های  کار با فایل را که کد آن را در بالا آوردیم به خاطر بیاوردید برای اینکه وقتی تسک createLogFile انجام شد و فایل txt جنریت شد تسک copyLogFile به طور خودکار اجرا شود و از فایل کپی بگیرد ، سپس بعد از کپی شدن فایل، تسک deleteLogFile به طور خودکار اجرا شود و txt اصلی را پاک کند باید از کدام روش استفاده کنیم ؟کد زیر را بررسی کنید :task(&#039;createLogFile&#039;) {
    doLast {
        val targetDir = layout.projectDirectory.dir(&#039;/generate&#039;).toString()
        File&#40;targetDir&#41;.mkdirs()
        File&#40;targetDir, &#039;log.txt&#039;&#41;.writeText(&#039;version = 1.0.0&#039;)
    }
    finalizedBy(&#039;copyLogFile&#039;)
}


task(&#039;copyLogFile&#039;, Copy::class) {
    val sourceDir = layout.projectDirectory.dir(&#039;/generate/log.txt&#039;).toString()
    val targetDir = layout.projectDirectory.dir(&#039;/src/main/resources&#039;).toString()
    from(sourceDir)
    into(targetDir)
    rename(&#039;log.txt&#039; , &#039;ConfLog.txt&#039;)
    finalizedBy(&#039;deleteLogFile&#039;)
}


task(&#039;deleteLogFile&#039;, Delete::class) {
    val targetDir = layout.projectDirectory.dir(&#039;/generate/log.txt&#039;).toString()
    delete(targetDir)
}اکنون با اجرای تسک createLogFile تمام تسک ها به صورت خودکار و زنجیر وار اجرا می شوند .اگر متوجه نشدید که چرا باید از روش finalize استفاده کنیم برگردید و دوباره کد هارا بررسی کنید و در نظر داشته باشید که اگر میخواستیم از dependOn استفاده کنیم باید تسک آخر (deleteLogFile) را اجرا می کردیم تا بتوانیم همین زنجیره را ایجاد کنیم .نکته ی کنکوری ****:در تصویر بالا مقابل تسک copyLogFile تگ UP_TO_DATE خورده است این به چه معناست ؟یکبار دیگر مکانیسم های کش گریدل را به خاطر بیاورید ?دلیل این اتفاق این است که من یکبار دیگر این تسک را اجرا کرده بودم و فایل ConfLog.txt در پوشه ی ریسورس ها موجود بود ، بنابراین گریدل مجددا آن را اجرا نکرد و صرفا از خروجی آن برای تسک بعدی استفاده کرد.تا این جای کار دانش خوبی در زمینه ی تسک های گریدل به دست آوردیم ، یکبار دیگه مباحث رو تو ذهنتون مرور کنید که میخواهیم عمق کار رو بیش تر کنیم تسک های پیشرفته (enhanced tasks)تا این جای کار تسک هامون رو مستقیم در فایل build.gradle تعریف میکردیم ، اما این تنها راه تعریف تسک ها نیست . اکنون با یک روش جدید آشنا شوید : open class PrintTask : DefaultTask() {
    @TaskAction
    fun sayHello() {
        println(&#039;hello from PrintTask&#039;)
    }
} متوجه شدید که میخواهیم چه کار کنیم ؟ قصد داریم تا تسک ها رو به صورت کد نویسی معمول انجام بدیم پس گام به گام نوشتن این نوع از تسک ها رو بررسی می کنیم :گام اول :  ابتدا buildSrc را ایجاد می کنیم . اما buildSrc چیست ؟ اگر بیلد گریدل خودتون رو به فرم kotlin dsl (kts) در آورده باشید احتمالا با این پوشه آشنا هستید .پوشه ی buildSrc یک entry point برای گریدل است . (در واقع گریدل در هر بیلد محتوای این پوشه را می خواند و کد های آن را تفسیر می کند)ما به این پوشه احتیاج داریم تا کد های تسک مون رو داخلش قرار بدیم ،نحوه ی ایجاد buildSrc : در داخل root پروژه ی خودتون یک دایرکتوری با نام buildSrc ایجاد کنیدسپس یکی از مسیر های زیر را در آن ایجاد کنید (وابسته به زبانی که در آن کد می زنید):rootProjectDir/buildSrc/src/main/groovyیاrootProjectDir/buildSrc/src/main/javaیا rootProjectDir/buildSrc/src/main/kotlinحالا یکبار گریدل رو سینک کنید اگر همه چیز درست باشد پوشه های .gradle و build برای شما جنریت می شود.گام دوم : در مسیری که در buildSrc ایجاد کردید کلاس خود را بنویسید (مانند PrintTask ) سپس آن را از ()DefaultTask اکستند کنید . **این کلاس حتما باید open باشد .**گام سوم : متدی را که در این کلاس اکشن شما را انجام می دهد بنویسید و با انوتیشن  @TaskAction علامت گذاری کنید .گام چهارم : اکنون برای کانفیگ و اجرای این تسک ، باید آن را در build.gradle رجیستر کنیم:task(&#039;sayHello&#039; , PrintTask ::class) {}
یا
tasks.register( &#039;sayHello&#039; , PrintTask ::class)به عنوان ارگومان اول یک اسم برای تسک انتخاب کرده که برای اجرای تسک از همین اسم استفاده می شود .(این اسم ترجیحا بهتر است تا با نام فانکشن TaskAction یکسان باشد )و به عنوان آرگومان دوم کلاسی را که ایجاد کردیم معرفی می کنیم .حالا با اجرای این تسک عبارت برای ما پرینت می شود .اما یک سوال : شاید بپرسید که چه لزومی داره تا به این روش تسک هارو بنویسیم ?همونطور که متوجه شدید مهم ترین مزیت این روش این بود که ما اکشن تسک را به جای اینکه در فایل build.gradle بنویسیم در یک متد جداگانه نوشتیم بنابراین از شلوغ شدن build.gradle جلوگیری کردیم و در آینده با پلاگین نوشتن بر روی این تسک می توانیم آن را با دیگران به اشتراک بگذاریم (تقریبا تمام تسک هایی که لایبرری های شخص ثالث به گریدل شما اضافه میکنند به این روش نوشته شدن)ارسال آرگومان به enhanced tasksبرای اینکه از طریق build.gradle مقادیری را برای تسک ارسال کنیم می توانیم از روش زیر استفاده کنیم :open class PrintTask : DefaultTask() {

    @Input var myName: String = &#039;  &#039;

    @TaskAction
    fun sayHello() {
        println(&#039;hello $myName&#039;)
    }
}همچنین در build.gradle:task(&#039;sayHello&#039; , PrintTask ::class) {
  myName = &#039;Mohsen&#039;
}به کمک انوتیشن @Input می توانیم هر مقداری را از build.gradle ارسال کنیم .حالا برای اینکه شما هم خسته نشید و مطلب رو کم کم جمع بندی کنیم ، با هم یک تسک مینویسیم و اون رو بررسی میکنیم :فرض کنید ما میخواهیم یک قرارداد در شرکتمون بنویسیم که طبق این قرارداد هیچ کس حق نداره از عکس ها با فرمت JPG در پروژه های اندرویدی مون استفاده کنه و اگر کسی این شرط رو نقض کرد گریدل ارور بده (یا یک ایمیل رو به لید پروژه ارسال کنه ) . کد این تسک به صورت زیر است :open class ExtensionFilterTask  : DefaultTask() {

    @Input var resDir: String = &#039;  &#039;
    @Input var taskEnabled: Boolean= true
    
    init {
        enabled = taskEnabled
    }

    @TaskAction
    fun findJPGExtension() {
        val file = File&#40;resDir&#41;
        file.list()?.forEach { fileName -&gt;
            if (fileName.contains(&#039;.JPG&#039;)) {
                throw GradleException(&#039;Invalid Extension&#039;)
            }
        }
    }

}build.gradle :tasks.register(
    &#039;findJPGExtension&#039;,
    ExtensionFilterTask::class
){
    resDir = layout.projectDirectory.dir(&#039;/src/main/resources/drawable&#039;).toString()
    taskEnabled = true
}
ابتدا کلاس ExtensionFilterTask را ایجاد کرده و ورودی های resDir که همان آدرس دایرکتوری resource/drawable ها است و taskEnabled را که مشخص می کند که تسک اجرا شود یا خیر را برای آن تعریف می کنیم .سپس در بلاک init مقدار enabled را ست می کنیم (مشخص می کند که تسک اجرا شود یا خیر)سپس متد اکشن را ایجاد کرده و داخل آن یک ابجکت از File با مسیر resDir  مشخص می کنیم متد list از ابجکت File اسم تمام فایل های موجود در دایرکتوری resDir  را بر می گرداند سپس با ایجاد یک حلقه ی for روی این لیست بررسی میکنیم که آیا فایلی با پسوند JPG وجود دارد یا خیر در صورت وجود چنین فایلی گریدل یک اکسپشن با مسیج Invalid Extension پرتاب می کند .ما تا این جا نکات مهم و کلیدی در رابطه با گریدل مطرح کردیم و شما بعد از خوندن این مقاله دانش خوبی در این رابطه پیدا می کنید ، اما این تازه شروع راهه ! گریدل یک ابزار قدرتمنده  شما می تونید تسک های مختلفی رو برای گریدل ایجاد کنید مثلا تسکی بنویسید که ایمیل ارسال کنه ، کامند ران کنه ، با نتورک ارتباط برقرار کنه ، برای شما فایل ارسال کنه ، کد های شما روبخونه و کد های دیپریکیت شده رو لیست کنه و گزارش بده ، تست های خاصی رو اجرا کنه و ...اگر مشتاق بودید تا در این رابطه بیش تر بدونید و پیش برید داکیومنت اصلی گریدل رو از دست ندید .ممنون که تا انتهای این مقاله طولانی همراه من بودید .??</description>
                <category>Mohsen Abedini</category>
                <author>Mohsen Abedini</author>
                <pubDate>Thu, 13 Apr 2023 21:22:00 +0330</pubDate>
            </item>
                    <item>
                <title>تست نویسی با Junit5 (بررسی فیچر ها و روش مهاجرت)</title>
                <link>https://virgool.io/@mohsenabedini79ooo/%D8%AA%D8%B3%D8%AA-%D9%86%D9%88%DB%8C%D8%B3%DB%8C-%D8%A8%D8%A7-junit5-%D8%A8%D8%B1%D8%B1%D8%B3%DB%8C-%D9%81%DB%8C%DA%86%D8%B1-%D9%87%D8%A7-%D9%88-%D8%B1%D9%88%D8%B4-%D9%85%D9%87%D8%A7%D8%AC%D8%B1%D8%AA-fd3cwkhhg2l6</link>
                <description>
اگر از اون دسته اندروید دولوپر هایی باشید که تست مینویسن ، احتمالا دارید از Junit4 برای تست های خودتون استفاده میکنید ، نمیدونم ! شاید هم به Junit5 مهاجرت کرده باشید ، به هر حال اگر که میخواهید درباره Junit5 بیش تر بدونید تو این مقاله با من همراه باشید .راستی اگر مقاله من درمورد unit test نویسی رو هم نخوندید یه نگاهی بهش بندازید .?اصلا Junit چیه ؟در ابتدا اجازه دهید تا تعریفی از Junit داشته باشیم :جی یونیت یک فریمورک اپن سورس است که برای نوشتن و اجرای تست ها در زبان جاوا به کار میرود.به کمک Junit framework میتوانیم با روش علامت گذاری (annotation) ، تست متد های خود را بنویسیم و همچنین با استفاده از test runner های فراهم شده توسط فریمورک آن ها را اجرا کنیم .از مزایای این فریمورک میتوان به : سادگی و سهولت در استفاده افزایش سرعت اجرای تست ها و بالابردن کیفیت آن ها استفاده از مکانیسم assertions برای بررسی نتیجه تست هااجرای خودکار و همچنین ارائه گزارش (report) فوری از نتیجه ی تست ها همونطور که میدونید Junit ورژن های مختلفی داره که هر کدوم از این ورژن ها با سایر ورژن ها تفاوت هایی داره . جدید ترین نسخه این فریمورک نسخه 5 آن است به گفته توسعه دهندگان آن یک نسخه programmer-friendly از این فریمورک است.مقایسه Junit4 با Junit5 : اگر بخواهیم بین Junit4 و Junit5 مقایسه ای انجام بدهیم به طور کلی :تغییرات انوتیشن ها در ورژن 5 در مقایسه با ورژن 4:در Junit5 برخی از انوتیشن های کاربردی اضافه شده اند که در آینده به بررسی آن ها خواهیم پرداخت.تغییر در معماری Junit5 :در Juint4 همه چیز در یک jar file باندل شده است (در واقع شامل یک معماری مونولیتیک است)در حالی که Junit5 از یک معماری مدرن چند لایه بهره برده است و شامل 3 زیر ماژول زیر است:JUnit Jupiter  - JUnit Platform  - JUnit Vintageتبدیل شدن runner ها در Junit4 به extension ها در Junit5:در Junit4 به کمک رانر ها میتوانستیم فیچر ها و فانکشنالیتی های مد نظر خودمان را به Junit اضافه کنیم  (مانند MockitoRunner یا SpringRunner) رانر ها در Junit4 از امکان رفلکشن استفاده می کردند .اما در Junit5 رانر ها به اکستنشن تبدیل شده اند .حذف Rule ها در Junit4 و تبدیل آن ها به extension در Junit5:رول ها کامپوننت هایی هستند که به کمک آن ها میتوانیم قبل یا بعد از اجرای یک تست یک عمل خاص را انجام بدهیم . (مانند InstantTaskExecutorRule)رول ها نیز در Junit5 به اکستنشن تبدیل شده اند .تغییر ورژن جاوای مورد نیاز :Junit 4 requires Java 5 or higher.Junit 5 requires Java 8 or higher.تغییر در Assertation ها :در Junit5 تعدادی assertion جدید نیز اضافه شده است .چرا باید به Junit5 مهاجرت کرد؟ مهم ترین دلیلی که میتونیم برای پاسخ به این سوال بیاریم اینه که در Junit5 امکاناتی مانند Repeated Tests, Dynamic Tests , Assumptions , Parameterized Tests , ...  اضافه شدند که باعث سهولت ، کارایی و افزایش کیفیت تست ها میشن و کمک میکنن تا از تست نویسی لذت بیش تری ببریم ?همچنین معماری ماژولار به کار رفته در Junit5 باعث میشه تا این فریمورک از قابلیت بسط پذیری و کاستومایز بسیار بالاتری نسبت به ورژن های قبلی برخوردار بشه .به کمک همین معماری ماژولار است که Junit 5 میتواند تست های نوشته شده با Junit 4 را اجرا و از آن ها پشتیبانی کند(backward compatibility)  که این مورد باعث میشود تا مهاجرت به آن نیز آسان شود.در Juint5 سعی شده تا به محدودیت ها و مشکلات Junit4 تا حد امکان توجه بشه و برای برطرف ساختن اون ها برنامه ریزی شده.تمامی این دلایل سبب شده تا Junit5 به یکی از ورژن های محبوب این فریمورک تبدیل بشه و تجربه ی جذابی رو برای توسعه دهنده هایی که ازش استفاده میکنن فراهم کنه.معماری Junit5 :یکی از موارد مهم در Juint5 آشنایی با معماری آن است . Junit5 از سه لایه (ماژول) تشکیل شده است:ماژولِ JUnit Platform (?) : این ماژول یک زیر ساخت بنیانی برای اجرا و لانچ تست ها در jvm فراهم می کند. به کمک Api های ارائه شده ، این ماژول به عنوان یک واسط بین Junit و client هایی مانند (build tools ها و ide ها)  عمل میکند. در صورتی که در Juint4 کلاینت ها مجبور به استفاده از رفلکشن برای دسترسی به Junit بودند.همچنین این ماژول امکان کاستومایز کردن Test Engine را بر روی Junit Platform میدهد تا لایبرری های شخص ثالث مانند Spock, Cucumber, FitNesse بتوانند مستقیما با Junit در ارتباط باشند و Test Engine های مختص خودشان را ارائه دهند.تست انجین(Test Engine) برای کشف و اجرای تست ها به کار میرود.ماژولِ Junit Jupiter (?) : فیچر ها ، اکستنشن ها و انوتیشن های جدیدِ Junit5 در این ماژول فراهم شده است.همچنین این ماژول به طور پیشفرض از Test Engine ارائه شده در ماژول platform استفاده میکند.ماژول Junit Vintage (?) :به کمک Engine ارائه شده در این ماژول میتوانیم تست ها نوشته شده با junit3 و Junit4 را نیز اجرا کنیم (backward compatibility) بنابراین اگر از تست های Junit3 یا Junit4 استفاده نمیکنیم نیازی نیست تا dependency این ماژول را به پروژه خود اضافه کنیم.برای آشنایی بیشتر با ماژول ها و زیرماژول های Junit5 لایبرری آن را بررسی کنید.مهاجرت به Junit5 :برای اجرای تست های Junit5 در اندروید از یک گردل پلاگین استفاده میکنیم .ابتدا دیپندنسی پلاگین را به گردل (سطح پروژه) خود اضافه میکنیم :dependencies {
    classpath(&amp;quotde.mannodermaus.gradle.plugins:android-junit5:1.8.2.0&amp;quot)
}همچنین در گردل (سطح ماژول) :plugins {
    id(&amp;quotde.mannodermaus.android-junit5&amp;quot)
}

dependencies {

// (Required) Writing and executing Unit Tests on the JUnit Platform
testImplementation(&amp;quotorg.junit.jupiter:junit-jupiter-api:5.8.2&amp;quot)

testRuntimeOnly(&amp;quotorg.junit.jupiter:junit-jupiter-engine:5.8.2&amp;quot)

// (Optional) If you need &amp;quotParameterized Tests&amp;quot
testImplementation(&amp;quotorg.junit.jupiter:junit-jupiter-params:5.8.2&amp;quot)

// (Optional) If you also have JUnit 4-based tests
  testImplementation(&amp;quotjunit:junit:4.13.2&amp;quot)
  testRuntimeOnly(&amp;quotorg.junit.vintage:junit-vintage-engine:5.8.2&amp;quot)

// (Required) For Mockito in  the JUnit Platform
testImplementation(&amp;quotorg.mockito:mockito-junit-jupiter:4.3.1&amp;quot)

}حالا به توضیح دیپندنسی ها خواهیم پرداخت:دیپندنسی (org.junit.jupiter:junit-jupiter-api:5.x.x) :به کمک jupiter api می توانیم از امکانات و انوتیشن های جدید Junit5 استفاده کنیم .دیپندنسی (org.junit.jupiter:junit-jupiter-engine:5.x.x) : برای کشف و اجرای تست ها به انجین Junit5 احتیاج داریم .دیپندنسی (org.junit.jupiter:junit-jupiter-params:5.x.x) : برای استفاده از پارامترایز تست ها که بعدا به توضیح آن خواهیم پرداخت به این دیپندنسی احتیاج داریم . اگر از این نوع تست ها استفاده نمیکنیم میتوانیم این دیپندنسی را اضافه نکنیم .دیپندنسی های (junit:junit:4.13.2 و org.junit.vintage:junit-vintage-engine:5.x.x) : در این جا دو سناریو مطرح است : اگر تست های خود را از ابتدا با Junit5 می نویسید احتیاجی به Junit4 و Vintage ندارید پس دیپندنسی های آن را  اضافه نکنید .اما اگر تست های Junit4 را در کنار Junit5 مینویسید برای استفاده از امکانات Junit5 و همچنین اجرای تست های Junit4 به دیپندنسی Vintage احتیاج دارید .دیپندنسی (org.mockito:mockito-junit-jupiter:4.x.x) : اگر در پروژه تان از ماکیتو استفاده می کنید برای initialize کردن انوتیشن های ماکیتو به MockitoExtension احتیاج دارید . که این اکستنشن در این لایبرری موجود است .سایر دیپندنسی های ماکیتو :testImplementation(&amp;quotorg.mockito:mockito-inline:x.x.x&amp;quot)
testImplementation(&amp;quotorg.mockito:mockito-core:x.x.x&amp;quot)حالا پروژه را سینک میکنیم تا دیپندنسی ها resolve بشوند.کار ما هنوز تمام نشده ? ممکن است بعد از اجرای اولین تست با ارور زیر مواجه بشویم :No tests found for given includes: [com.example.......XXXXX]خصوصا در مواردی که از parameterized test ها استفاده کنیم وقوع این ارور محتمل است. برای رفع آن باید یک تسک به تسک های گردل اضافه کنیم .بنابراین در گردل سطح ماژول کد زیر را اضافه میکنیم ://kotlin dsl

plugins { }
android { }

tasks.withType&lt;Test&gt; {
    useJUnitPlatform()
}//groovy gradle

 plugins { } 
 android { } 
 
test {
    useJUnitPlatform()
}حالا مجددا گردل را سینک میکنیم . تا این جا دیپندنسی های لازم را اضافه کرده ایم و Junit5 آماده است ، یک تست ساده را اجرا کنید اکنون نباید با اروری مواجه شوید .نوشتن اولین تست :در ابتدا همانطور که از مقاله قبل به خاطر دارید در Junit4 برای نوشتن تست ها از ساختار زیر استفاده می کردیم :@SmallTest
@ExperimentalCoroutinesApi
@RunWith(MockitoJUnitRunner::class)
class viewModelTest {

@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()

@get:Rule
val coroutineTestRule = CoroutineTestRule()

@Test
fun test(){  //write your test  }

}اما همانطور که گفته شد در Junit5 رانر ها (RunWith) و رول ها (Rule)حذف و به اکستنشن تبدیل شده اند.بنابراین بازنویسی کد بالا در Juint5 به صورت زیر می شود:@SmallTest
@ExperimentalCoroutinesApi
@ExtendWith(
    InstantExecutorExtension::class,
    CoroutineTestExtension::class,
    MockitoExtension::class
)
class viewModelTest{

@Test
fun firstTest(){   //write first test  }

} همانطور که در کد بالا مشاهده می کنید برای رول های InstantTaskExecutorRule و CoroutineTestRule اکستنشن های مجزا نوشتیم و آن هارا در ExtendWith معرفی کرده ایم .اگر از کاربرد این رول ها مطلع نیستید مقاله قبل درمورد Unit Test ها را مطالعه کنید .برای نوشتن InstantExecutorExtension کلاس زیر را به پکیج تست خود اضافه کنید :import android.annotation.SuppressLint
import androidx.arch.core.executor.ArchTaskExecutor
import androidx.arch.core.executor.TaskExecutor
import org.junit.jupiter.api.extension.AfterEachCallback
import org.junit.jupiter.api.extension.BeforeEachCallback
import org.junit.jupiter.api.extension.ExtensionContext

class InstantExecutorExtension : BeforeEachCallback, AfterEachCallback {

    @SuppressLint(&amp;quotRestrictedApi&amp;quot)
    override fun beforeEach(context: ExtensionContext?) {
        ArchTaskExecutor.getInstance()
            .setDelegate(object : TaskExecutor() {
                override fun executeOnDiskIO(runnable: Runnable) = runnable.run()
                override fun postToMainThread(runnable: Runnable) = runnable.run()
                override fun isMainThread(): Boolean = true
            })
    }

    @SuppressLint(&amp;quotRestrictedApi&amp;quot)
    override fun afterEach(context: ExtensionContext?) {
        ArchTaskExecutor.getInstance().setDelegate(null)
    }

}
همچنین برای CoroutineTestExtension نیز به همین صورت عمل می کنیم :import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.jupiter.api.extension.AfterEachCallback
import org.junit.jupiter.api.extension.BeforeEachCallback
import org.junit.jupiter.api.extension.ExtensionContext

@ExperimentalCoroutinesApi
class CoroutineTestExtension(
    private val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
) : BeforeEachCallback, AfterEachCallback, TestCoroutineScope by TestCoroutineScope(dispatcher) {

    override fun beforeEach(context: ExtensionContext?) {
        Dispatchers.setMain(dispatcher)
    }

    override fun afterEach(context: ExtensionContext?) {
        cleanupTestCoroutines()
        Dispatchers.resetMain()
    }
}
برای اضافه کردن این اکستنشن به دیپندنسی (کروتین - تست) احتیاج دارید :testImplementation(&amp;quotorg.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.3&amp;quot)تا این جای کار اکستنشن ها را به برنامه خود اضافه کردیم حالا به سراغ MockitoExtension میرویم .این اکستنشن از دیپندنسی (org.mockito:mockito-junit-jupiter:4.x.x) که سابقا برای کانفیگ Junit5 به گردل اضافه کردیم در دسترس است .کاربرد این اکستنشن initialize کردن انوتیشن های ماکیتو است .اگر این اکستشن را به Junit اضافه نکنیم باید قطعه کد زیر را جایگزین آن کنیم:@BeforeEach
fun setup(){
    MockitoAnnotations.openMocks(this)
}اکنون Junit آماده ی اجرای یونیت تست هاست .???در این جا لازم است به دو نکته توجه کنیم: برخی از انوتیشن های کاربردی در Junit5 تغییر کرده اند :بنابراین به جای استفاده از Before@ از BeforeEach@ استفاده می کنیم . متدی که با انوتیشن BeforeEach@ علامت گذاری می شود قبل از اجرای هر تست متد یکبار اجرا میشود .متدی که با انوتیشن AfterEach@ علامت گذاری می شود بعد از اجرای هر تست متد یکبار اجرا میشود.متدی که با انوتیشن BeforeAll@ علامت گذاری میشود قبل از اجرای همه ی تست متد ها یکبار اجرا میشود .متدی که با انوتیشن AfterAll@ علامت گذاری میشود بعد از اجرای همه ی تست متد ها یکبار اجرا میشود. دقت شود که هنگام استفاده از انوتیشن Test@ ، انوتیشن مربوط به jupiter api ایمپورت شود .اگر از Test@ موجود در ای پی ای junit4 (org.junit) استفاده شود .در صورتی که دیپندنسی Vintage را اضافه کرده باشیم تست ما با Junit4 اجرا خواهد شد . اما در غیر این صورت تست fail شده و از ما خواسته میشود تا Vintage را به دیپندنسی های خود اضافه کنیم .بنابراین اولین تست خود را به صورت زیر مینویسیم :import androidx.test.filters.SmallTest
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.junit.Assert
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.junit.jupiter.MockitoExtension


@SmallTest
@ExperimentalCoroutinesApi
@ExtendWith(MockitoExtension::class)
class Junit5UnitTestExample {

    @Mock
    lateinit var list: List&lt;String&gt;

    companion object {
        const val fakeElement = &amp;quotELEMENT&amp;quot
    }

    @BeforeEach
    fun init(){
        Mockito.`when`(list[0]).thenReturn(fakeElement)
    }


    @Test
    fun firstTest() {
        val firstItem:String=list[0]
        Assert.assertTrue( firstItem.startsWith(&amp;quotE&amp;quot) )
    }

}
در این تستِ ساده با Mock@ یک لیست از نوع استرینگ را ماک کرده ایم و در متد init از ماکیتو خواسته ایم تا هر وقت عنصر 0 این ارایه خواسته شد استرینگ fakeElement را برای ما بازگرداند همچنین در firstTest بررسی کرده ایم که ایا fakeElement با کاراکتر E شروع شده است یا خیر .این تست به دلیل درستی شرط ( firstItem.startsWith(&quot;E&quot;)  ) پاس میشود.نکته : در این تست کلاس ، اکستنشن های (CoroutineTestExtension و InstantExecutorExtension) را اضافه نکرده ایم زیرا از کروتین ها و یا کامپوننت هایی که ترد mian را کال میکنند استفاده نشده است. آشنایی با فیچر های Junit5 :حالا به سراغ بررسی امکانات جدید و جذاب Junit5 می رویم .1) Disabling Testsبه کمک انوتیشن Disabled@ میتوانیم تست کلاس ها یا تست متد های خود را ignore کنیم .کاربرد این انوتیشن در مواردی است که یک یا برخی از تست های ما به مشکل خورده اند اما تصمیم داریم تا آن هارا در آینده فیکس کنیم بنابراین به جای کامنت کردن تست ها آن هارا ignore میکنیم تا توسط Junit اجرا نشوند .همچنین در مواردی که از CI CD استفاده میکنیم ممکن است در برخی از موارد بخواهیم تست های مشکل دار را Skip کنیم تا بتوانیم ادامه ی pipeline را پیش برویم بنابراین میتوانیم از این امکان بهره ببریم.2) Conditional Test Executionگاهی از اوقات ممکن است احتیاج داشته باشیم تا برای اجرای تست ها شرایطی را تعیین کنیم تا درصورتی که آن شرط برقرار بود تست ما اجرا شود . Junit5 به طور پیشفرض برخی از این شرایط را تعریف کرده است :Operating System Conditionsبه کمک انوتیشن های EnableOnOs@ و یا DisableOnOs@ میتوانیم اجرای تست های خود را به یک یا چند سیستم عامل خاص محدود کنیم : Java Runtime Environment Conditionsبه کمک انوتیشن های EnabledOnJre@ و DisabledOnJre@ و EnabledForJreRange@ و DisabledForJreRange@ میتوانیم اجرا شدن تست ها را روی ورژن های مختلف Jre کنترل کنیم :Custom Conditionsدر Junit5 تست های شرطی دیگری نیز وجود دارند که به ذکر همه ی آن ها نپرداختیم ، اما نکته مهم این است که میتوانیم Custom Conditions های پیشفرض خود را بنویسم . به مثال زیر توجه کنید :3) Tagging and Filteringبه کمک انوتیشن Tag@ میتوانیم تست کلاس ها و تست متد های خود را تگ بزنیم . به کمک این تگ ها میتوانیم تست های خود را دسته بندی کنیم و یا حتی اجرای تست ها را روی تگ های خاص فیلتر کنیم .به کمک includeTags که به تسک Test در گردل اضافه شده است Junit فقط تست ها با تگ های مشخص شده را اجرا می نماید .4) Test Execution Orderگاهی ممکن است بخواهیم تا ترتیب مشخصی را برای اجرای تست متد های خود به کار ببریم ، به کمک انوتیشن TestMethodOrder(MethodOrderer.OrderAnnotation::class)@  در Junit5 این امکان فراهم شده است.انوتیشن های Order@ اولویت اجرای تست ها را مشخص میکنند . این امکان برای nested test ها نیز فراهم شده است فقط کافیست تا از انوتیشن (ClassOrder.OrderAnnotation::class)TestClassOrder@ استفاده کنیم. 5) Display Namesبه کمک این انوتیشن که برای تست کلاس ها و تست متد ها به کار می رود میتوانیم اسم تست ها را کاستومایز کنیم و یا کارکتر های خاص و ایموجی ها را به آن اضافه کنیم تا در ریپورت و اجرای تست ها نمایش داده شوند.از این امکان برای بالا بردن خوانایی و زیبایی تست های نوشته شده استفاده می شود . همچنین به کمک انوتیشن DisplayNameGeneration@ نیز میتوانیم اسامی تولید شده توسط تست ها را با شرایطی خاص کنترل کنیم .به طور نمونه در مثال زیر تمامی UnderScore ها را از اسامی تست ها با Space جایگزین کرده ایم :@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class Junit5_Diplay_Name_Generator_Test {
  
  @Test
  void test_Add() {
    assertEquals(5, MathUtil.add(3, 2));
  }
  
  @Test
  void test_Multiply() {
    assertEquals(15, MathUtil.multiple(3, 5));
  }

  @Test
  void test_Devide() {
    assertEquals(5, MathUtil.devide(25, 5));
  }
  
  @Test
  void test_IsPrime() {
    assertTrue(MathUtil.isPrime(13));
  }
} همچنین Juint5 این امکان را فراهم کرده است تا بتوانیم DisplayNameGeneration ها را کاستومایز کرده و NameGeneration خودمان را بنویسیم .6) Assumptionsدر واقع Assumption ها ضمانتی بر اجرا یا عدم اجرای تست ها است . به کمک عبارات assumeTrue , assumeFalse , assumingThat درستی یک شرط را بررسی کرده ، اگر شرط برقرار بود تست اجرا میشود و در غیر این صورت تست fail نمیشود بلکه توسط جی یونیت abort می شودفرض کنید تستی را نوشته ایم که نباید در لوکال اجرا شود و فقط باید در سرور های CI اجرا شود.به کمک Assumptions میتوانیم اجرای این تست را مدیریت کنیم :@Test
 void testOnlyOnCiServer() {   
         assumeTrue(&amp;quotCI&amp;quot.equals(System.getenv(&amp;quotENV&amp;quot))); 
        // remainder of test  
 }  این تست فقط زمانی اجرا می شود که شرط داخل assumeTrue برقرار شود.اما آیا به کمک Custom Conditions ها که بالاتر به معرفی آن پرداختیم نمی توانستیم اجرای این تست را کنترل کنیم ؟بله می توانستیم !اما مثال زیر را در نظر بگیرید :میخواهیم تستی بنویسیم تا اگر در محیط سرور CI بودیم یک Assertation خاص را به اضافه Assertation ای که در سایر محیط ها اجرا می شود نیز اجرا کند .در این صورت فقط میتوانیم از امکان Assumption و متد assumingThat استفاده کنیم .7) Nested Testsبه کمک انوتیشن Nested@ می توانیم در یک تست کلاس ، تست کلاس های دیگری را (Nested) ایجاد کنیم.کاربرد این انوتیشن در مواردی است که میخواهیم تست های مشابه را گروه بندی کنیم و برای آن ها ساختار مشخصی ایجاد کنیم . به مثال زیر توجه کنید :8) Repeated Testsیکی از قابلیت های جالب Junit5 تست های تکرار شونده است. به کمک انوتیشن RepeatedTest@ میتوان یک تست را به تعداد دفعات مشخص شده تکرار کرد .این نوع از تست ها در مواردی که ممکن است یک عمل پس از چند بار اجرا ، نتیجه ی متفاوتی داشته باشد و یا در مواردی که اجرای تست ها به زمان سیستم وابستگی دارد کاربرد بسیار زیادی دارد.@RepeatedTest(5)
public void testRepeatedTest() {
System.out.println(&amp;quotHello World&amp;quot);
}9) Parameterized Testsاین قابلیت کاربردی Junit5 باعث میشود تا بتوانیم یک تست را با آرگومان های متفاوت تکرار کنیم ??.برای ایجاد این تست ها باید تست متد را به جای Test@ با انوتیشن ParameterizedTest@ علامت گذاری کنیم .همچنین آرگومان های مورد نظر را با انوتیشن ValueSource@ مشخص می کنیم:@ValueSource(strings = { &amp;quotracecar&amp;quot, &amp;quotradar&amp;quot, &amp;quotable was I ere I saw elba&amp;quot })انوتیشن ValueSource از type های زیر پشتیبانی میکند :• short  • byte  • int  • long  • float  • double  • char  • boolean  • java.lang.Stringهمچنین باید در نظر داشته باشید که تست متد های ما در Parameterized Test ها باید ارگومانی از جنس type مشخص شده برای ValueSource داشته باشند :@ValueSource(ints = { 1, 2, 3 }) 
void testWithValueSource(int argument)@ValueSource(strings = { &amp;quotracecar&amp;quot, &amp;quotradar&amp;quot, &amp;quotable was I ere I saw elba&amp;quot })
 void palindromes(String candidate)حالا به مثال زیر توجه کنید :این تست که palindrome بودن string ها را چک می کند ، 3 بار با مقادیر مشخص شده در ValueSource اجرا میشود .اگر بخواهیم تست های خود را با ارگومان های بیش تری اجرا کنیم به جای استفاده از ValueSource از CsvSource استفاده میکنیم .در CsvSource ارگومان ها در یک دابل کوتیشن و با علامت کاما از هم متمایز میشوند . به مثال زیر توجه کنید : در این مثال در هر اجرای تست متد ، آرگومان های زیر به fruit , rank داده میشود .10) Dynamic Testsتست های استانداردی که با انوتیشن Test@ علامت گذاری می شوند استاتیک هایی هستند که در زمان کامپایل به طور کامل مشخص می شوند .اما DynamicTest ها که با انوتیشن TestFactory@ علامت گذاری میشوند در زمان RunTime جنریت میشوند.هر تست متدی که با این انوتیشن علامت گذاری می شود باید یک Stream, Collection, Iterable, Iterator ای از نوع DynamicTest را به عنوان خروجی بر گرداند هر نوع خروجی ، غیر از موارد مشخص شده سبب وقوع یک JUnitException میشود زیرا خروجی های غیر مجاز در زمان کامپایل قابل شناسایی نیستند.اما کاربرد این گونه از تست ها چیست ؟ داینامیک تست ها در مقایسه با تست های استاندارد از انعطاف پذیری بالاتری در نحوه تولید ورودی ها و همچنین نحوه اجرای تست ها برخوردار است .به کمک این نوع از تست ها می توانیم در یک تست متد ، تست های متفاوت دیگری را اجرا کنیم .همچنین در مواردی که احتیاج داریم تا زمان اجرای تست ها را از زمان کامپایل به ران تایم ببریم از این تست ها استفاده میکنیم .بهره گیری از این تست ها سبب بهبود کیفیت تست ها و همچنین پرهیز از تکرار تست متد های مشابه می شود .?نحوه ی تعریف تست های داینامیک :در گام اول تست متد را به جای Test@ با TestFactory@ علامت می گذاریم .در گام دوم خروجی تست متد را ازtype های مجاز برای داینامیک تست ها مشخص میکنیم . به طور مثال :List&lt;DynaimcTest&gt;Collection&lt;DynaimcTest&gt;Stream&lt;DynaimcTest&gt; ....در گام سوم از متد استاتیک org.junit.jupiter.api.DynamicTest.dynamicTest برای ساختن داینامیک تست  ها استفاده می کنیم . هر متد dynamicTest شامل دو قسمت است : یک String که نام تست را مشخص میکند  دوم یک Executable که برای تعریف assertation به کار می رود .اکنون برای درک بیش تر موارد گفته شده مثال زیر را بررسی می کنیم :در این مثال در ابتدا از انوتیشن TestFactory برای علامت گذاری تست متد استفاده کرده ایم .سپس خروجی متد را لیستی از DynamicTest مشخص کرده ایم .حالا یک لیست شامل دو DynamicTest را بر گردانده ایم .هر داینامیک تست شامل یک اسم و یک کد اجرایی جهت مشخص کردن نتیجه تست است .بعد از اجرای تست متد ()testPointsDynamically ، دو داینامیک تست مخشص شده اجرا میشود و نتیجه ی آن ها در IDE نمایش داده میشود .در واقع با اجرای یک تست متد ، دو تست متفاوت با نتیجه های متفاوت را اجرا کردیم .حالا مثال های زیر را بررسی کنید :مراقب لایف سایکل DyanamicTest ها باشید? :لایف سایکل اجرای داینامیک تست ها با تست های استاندارد کاملا متفاوت است.به ازای هر dynamic test موجود در یک تست متد ، callback های لایف سایکل وجود ندارد .این بدان معناست که متد های BeforeEach@ و AfterEach@ فقط یکبار به ازای تست متدی که با انوتیشن TestFactory@ مشخص شده است اجرا می شوند نه به ازای هر dynamicTest به کار رفته در آن !الگوی اجرای تست های استاندارد و داینامیک به صورت زیر است :11) Timeoutsبه کمک انوتیشن Timeout@ می توانیم زمان مشخصی را برای اجرای تست ها مشخص کنیم تا اگر زمان اجرا از حد مجاز فراتر رفت تست fail شود.واحد زمان برای این انوتیشن به طور پیشفرض ثانیه است که این مورد به کمک پارامتر unit قابل تغییر است .مشخصا بررسی کامل Parameterized Tests و Dynamic Tests نیازمند دو مقاله جداگانه است ، اما در این جا جهت آشنایی و داشتن یک دید کلی از این فیچر ها و کاربردشان آن ها را بررسی کردیم .همچنین برای بررسی سایر امکانات به کار رفته در Junit5 به راهنمای کاربری آن مراجعه کنید. در این مقاله نیز برای بسیاری از مثال های به کار رفته از فیچر های Junit5 از مثال های راهنمای کاربری آن استفاده شد.اگر که هنوز از Junit4 استفاده می کنید امیدوارم تا با خوندن این مقاله علاقه مند به استفاده از Junit5 شده باشید .مرسی که تا آخر مقاله همراه من بودید .?</description>
                <category>Mohsen Abedini</category>
                <author>Mohsen Abedini</author>
                <pubDate>Wed, 01 Jun 2022 13:38:37 +0430</pubDate>
            </item>
                    <item>
                <title>چطور unit test نوشتیم ؟</title>
                <link>https://virgool.io/codenevis/%DA%86%D8%B7%D9%88%D8%B1-unit-test-%D9%86%D9%88%D8%B4%D8%AA%DB%8C%D9%85-l0zdmbdrkdip</link>
                <description>در این مقاله به بررسی روش ها ، ابزار ها و چالش های تست نویسی که خودمان با آن ها مواجه شدیم خواهیم پرداخت .همچنین نیاز است که آشنایی اندکی با unit test ها در اندروید داشته باشید و حداقل چند تست نوشته باشید .چرا باید تست بنویسیم؟بزارید مقاله رو با این سوال شروع کنیم ، اصلا چرا باید تست بنویسیم ؟خب این سوال قبلا خیلی مطرح تر بود و هر وقت در مورد تست صحبت میکردی یه عده دولوپر ، پا برهنه میپریدن وسط صحبت و کلا لزوم تست نوشتن برای اپلیکیشن ها رو زیر سوال میبردن .اما مدتی بعد وقتی شرکت های بزرگ تر تست نویسی رو وارد استاندارد های توسعه خودشون کردن و برنامه نویس هارو ملزم به تست نویسی کردن فضا عوض شد .کم کم حتی شرکت های دیگه هم شروع به اجرای این قوانین کردن و این فرض که تست نوشتن فقط برای اپلیکیشن های بزرگه رو کم رنگ کردن و سوال چرا باید تست بنویسیم به سوال چطور تست بنویسیم تغییر پیدا کرد .خوشبختانه در ایران هم شرکت ها به این سمت حرکت کردن و دیگه دیدن اگهی استخدامی که یکی از شرایطش تست نوشتن باشه تعجب برانگیز نیست . اما بریم سراغ سوالمون :بزارید با یه مثال شروع کنیم ، فرض کنید یه خودرو خریدید ، یکی از اقداماتی که باید انجام بدید بیمه کردن ماشینتونه . احتمالا کار سختیه چون تقریبا هممون میدونیم که پولی که میپردازیم یه هزینه اضافیه اما ایا این هزینه رو نمیپردازیم ؟ ما حتی ماشینمون رو بدون بیمه تو خیابون هم نمیاریم . چرا ؟چون این شرط عقله (یکبار این هزینه رو میپردازی اما تا مدت ها خیالت راحته)تست نوشتن هم شاید از نظر بعضی ها یک هزینه ی اضافی به نظر برسه ایدشون هم اینه که زمانی که برای یادگیری ، نوشتن و نگهداری تست ها گذاشته میشه زمان زیادیه و همون زمان رو میشه برای توسعه فیچر های جدید گذاشت تجربه چندین ساله دنیای نرم افزار ثابت کرده که زمانی که برای تست ، ریفکتور و استاندارد کردن کد ها گذاشته میشه به هیچ عنوان هدر دادن زمان نیست بلکه حتی باعث حفظ زمان شما نیز میشود .مثال بیمه ماشین رو به خاطر بیارید ، آیا شما آن هزینه رو پرداخت نکردید ؟ ؟در مورد این سوال ساعت ها میشه بحث کرد اما اجازه بدید تا به ذکر موردی برخی از ویژگی های تست نوشتن اکتفا کنیم:تست نوشتن خیال مارو از تاثیرات نا خواسته کد ها بر یکدیگر(side effect) تا حد زیادی راحت میکنه .تست نویسی به اجایل بودن تیم کمک زیادی میکنه و زمان دیباگ رو کاهش میده .تست نویسی کیفیت کد را افزایش میدهد و باعث رعایت اصول solid و yagni در کد میشودتست نویسی اثباتی بر عملکرد کد نوشته شده است و خیال برنامه نویس ها رو از عملکرد کد راحت میکنه برنامه نویسی که تست مینویسد با کد تست پذیر آشنا است و کد هایی که مینویسد از کیفیت بالاتری برخوردارند .و ...مشخصا تست نوشتن ویژگی های زیادی داره که ذکر همه ی آن ها نیاز به یک مقاله جدا داره اما اینکه ما از همه ی خواص تست نویسی بهره مند بشیم به تست هایی که نوشته ایم بستگی دارد آیا تست های ما اصولی بوده اند ؟  آیا تمام سناریو های احتمالی را بررسی کرده ایم ؟ ایا تست های ما code coverage خوبی داشته اند ؟چطور باید تست(unit test) بنویسیم ؟تست نوشتن یک مهارته ، چیزی نیست که یک شبه بشه بهش رسید و نیاز به تلاش مداوم داره هر چه قدر هم که در تست نوشتن تجربه داشته باشید نمیتونید تضمین کنید که میتونید برای هر کدی تست بنویسید چون اونوقته که ، سوپرایز سوپرایز یه چالش جدید بهتون لبخند میزنه همچنین تست نوشتن و سرو کله زدن با چالش هاش میتونه خیلی جذاب باشه.خصوصا لحظه ای که اون تست قرمزه که کلی درگیرش شده بودید سبز میشه .حالا اجازه بدید تا یک فرمول اساسی برای نوشتن یونیت تست ها اراِئه بدیم :1)  از کلاسی که میخواهید برای آن تست بنویسید ، آبجکت بگیرید(ماک نکنید)2) دیپندنسی ها و api های اندروید رو که بهشون احتیاجی ندارید رو ماک کنید 3) عملکرد کد را به لحاظ آن چه مورد انتظار است بررسی کنید (کاری به implementation نداشته باشید)اکنون به توضیح هر مورد خواهیم پرداخت:از کلاسی که میخواهید برای آن تست بنویسید ، آبجکت بگیرید(ماک نکنید)مفهوم این جمله ، ساده اما مهم است فرض کنید که میخواهید برای کلاس A تست بنویسید ، شما به ابجکتی از آن کلاس نیاز دارید که شامل تمام خواص آن کلاس از جمله پیاده سازی متد ها و ... باشد اگر آن را ماک کنید ، آبجکت ماک شده همه ی خواص آن کلاس را نخواهد داشت .(دلیلش رو بعدا توضیح میدیم)دیپندنسی ها و api های اندروید رو که بهشون احتیاجی ندارید رو Mock کنیددر این جا انتظار میره که شما با Mockito کار کرده باشین و باهاش آشنا باشید هر چه قدر که درک بهتری از ماکیتو داشته باشین تست نوشتن براتون کار راحت تری میشه معمولا بسیاری از چالش ها و ارور های unit test ها مربوط به همین ماکیتو است بنابراین سعی میکنیم در این مقاله با مفاهیم ماکیتو و چالش های اون بیش تر آشنا بشیم ابتدا ماک کردن رو بیش تر توضیح میدیم :فرض کنید که کلاسی به نام A دارید که قراره محاسباتی رو روی یه عدد انجام بده و اون عدد رو خروجی بده در قسمتی از از این کد از تابعی استفاده کردید که عمل جمع رو انجام میده و این تابع مربوط به api خود اندرویده (شما ننوشتیدش) class A{
fun cal(input : Int):Int{
// محاسبات شما 
val num=Math.sum(input)
//محاسبات شما
}
}در مثال بالا فرض کنید که sum مربوط به api های اندرویده حالا شما میخواهید برای کلاس A تست بنویسید تا ببینید به ازای input مثلا 2 خروجی تابع cal  برابر با 100 میشود یا نه .در این مثال ما باید تست رو با فرض درست بودن تابع sum انجام بدیم چون هدف ما تست عملکرد جمع نیست در این جا عملکرد تابع sum اهمیتی در تست ما نداره و فقط خروجی اون مهمه پس ما اون رو ماک میکنیم و خروجی ای که لازم داریم رو براش مشخص میکنیمval mock=Mockito.mock(Math::class)
Mockito.when(mock.sum(2)).thenReturn(5)در رشته کد بالا ما از کلاس Math یک ابجکت ماک شده گرفتیم و برای تابع sum از کلاس math تعریف کردیم که به ازای ورودی 2 به ما خروجی 5 دهد .حالا به کلاس A بر گردیم حین اجرای تست ، وقتی به تابع sum میرسیم بدون در نظر گرفتن عملکرد sum خروجی 5 برای ما return میشود و متغیر num مقدار 5 می گیرد.حالا اگر کلاس Math وتابع sum رو خودمون نوشته بودیم چه میشد؟با فرض صحیح بودن عملکرد آن باز هم آن را ماک میکردیم زیرا عملکرد تابع sum در تست ما موضوعیتی نداره اگر به دنبال بررسی عملکرد تابع sum هستید باید برای کلاس Math تستی جداگونه بنویسید و عملکرد آن را در اون تست مورد بررسی قرار بدهید .نه در کلاس A که هدف دیگری را دنبال میکند .تا این جا باید با دلیل ماک کردن دیپندنسی ها آشنا شده باشید ، حالا با آبجکت های ماک بیش تر آشنا میشویم:تفاوت آبجکت های ماک شده با آبجکت های دیگر چیست ؟توجه به این مفهوم بسیار بسیار مهم است و از مشکلات بسیار زیادی در نوشتن تست ها جلوگیری می کند .به شکل زیر نگاه کنید :virtual object vs Real Objectکادر آبی رنگ یک آبجکت حقیقی از کلاس A و قسمت زرد رنگ آبجکت ماک شده از همان کلاس A را نشان میدهد.چه تفاوتی میبینید ؟ بله آبجکت ماک شده شامل پیاده سازی متد ها نیست و از آن ها خبر ندارد اگر بگوییم A.sum(1,2) چه اتفاقی می افتد ؟ واضح است مقدار محاسبه میشود اما اگر همین کار را با ابجکت ماک شده کنیم چه اتفاقی می افتد ؟تابع sum همواره نال بر میگرداند و Mockito ارور زیر را میدهد wanted but not invoked ..there is Zero interaction with this mockاین ارور را در ذهن خود داشته باشید ، احتمالا با آن زیاد مواجه خواهید شد اما چرا ؟ این منطقی است ، وقتی ماک میکنید به این معناست که عملکرد آن کد برایتان مهم نیست فقط به دنبال آن هستید که آن قطعه کد انتظار شما را بر آورده کند اصل اول فرمول unit test نویسی که در بالا به آن اشاره شد را به خاطر بیاورید به همین خاطر گفتیم که کلاسی که میخواهید تست کنید را ماک نکنید !!اما برای رفع این ارور باید چه کاری انجام بدهیم ؟ساده است ، انتظار خودتان از آن متد را مشخص کنید . اینگونه :Mockito.when(mockObject.sum(1,2)).thenReturn(3)اکنون با خیال راحت از ماک آبجکت خود استفاده کنید .به همین خاطر است که به ابجکت های ماک شده ، آبجکت مجازی نیز گفته میشود .اسپای(Spy)؟البته در Mockito آبجکت دیگری تحت عنوان Spy نیز وجود دارد .این ها (Spy) به آبجکت های جاسوس نیز معروفند . Spy ها به نوعی در میان Real Object ها و Mock Object قرار میگیرند و عملکردی دوگانه دارند .برای ماک کردن اینترفیس ها مجبور به استفاده از Spy هستید ، چون اینترفیس ها کلاس های بدون پیاده سازی اند.تا اینجای کار با فهم عملکرد ماکیتو  از مشکلات بزرگی جلوگیری کردیم :حالا در این جا به نکته ی کوچکی اشاره میکنم :درست است که ماک آبجکت ها فاقد پیاده سازی کد ها هستند اما آیا میتوان برای کدی که عملکرد اشتباهی دارد ماک درست کرد و با Mockito.when از آن انتظار خروجی درست داشت ؟ خیر ماکیتو اینلاین (Mockito_inline) چیست؟Mockito final errorاصولا ماکیتو توانایی ماک کردن کلاس های final را ندارد , از آن جایی که کلاس ها در کاتلین به طور پیشفرض final هستند برای جلوگیری از ارور باید لایبرری mockito - inline را به پروژه کاتلینی خود اظافه کنید:// https://jarcasting.com/artifacts/org.mockito/mockito-inline/
implementation(&amp;quotorg.mockito:mockito-inline:4.2.0&amp;quot)این لایبرری مثل یک اکستنشن به Mockito اصلی پروژه اظافه میشه و باعث میشه که دیگه ارور final کلاس هارو نبینید .حتی اگر تست هاتون بدون مشکل اجرا میشن فراموش نکنید که این لایبرری رو به پروژه های کاتلینی خودتون اظافه کنید .?بررسی ارور  &quot;Method ... not mocked&quot;  (** یک نکته بسیار مهم **)این یکی از ارور های رایج در استفاده از ماکیتو است که میگوید &quot;متد ماک نشده اما از آن استفاده شده&quot;ابتدا یکی از شرایط وقوع این مشکل را بررسی می کنیم ، فرض کنید که در بخشی از کدتان که قصد تست آن را دارید از یکی از api های اندروید استفاده کرده اید (منظور کد های موجود در android sdk است مثلا از کلاس Intent یا...)خب همانطور که قبلا گفته شد  آن را ماک می کنیم اما حین اجرا باز هم به ارور Method Not Mocked بر میخوریم مشکل از کجاست ؟ ما که آن را ماک کرده ایم?اگر خود کلاسی (از Api اندروید) که ماک کرده اید به یک کلاس دیگر دیپندنسی داشته باشد  آن وقت چه ؟واضح است باید دیپندنسی آن را نیز ماک کنیم تا به ارور Method Not Mocked بر نخوریم اما یک سول مهم : اگر تعداد دیپندنسی ها زیاد بود یا ماک کردن همه ی آن ها کاری پیچیده بود آن وقت چه ؟راحت است ! کد زیر را به گردل (سطح ماژول) اظافه کنید ://kotlin

android{
testOptions {    
 unitTests{     
    isReturnDefaultValues=true    
     }
  }
}//Groovy

android {
  ...
  testOptions {
    unitTests.returnDefaultValues = true
  }در کد بالا ما به گردل گفتیم که در یونیت تست ها مقدار های پیشفرض return شوند ، اما چرا؟اجازه دهید تا با جزئیات بیش تری این مورد را بررسی کنیم :اندروید برای اجرای یونیت تست ها از فایل android.jar استفاده میکند . این فایل شامل هیچ کد حقیقی ای نیست و  از پیاده سازی کد ها اطلاعی ندارد  ، فقط از آنها یک تصویر در اختیار دارد . به همین دلیل تمام متد های sdk به طور پیشفرض exception پرتاب میکنند .با اظافه کردن کد بالا به گردل شما رفتار android.jar را تغییر دادید یعنی در متد ها به جای پرتاب اکسپشن مقدار دیفالت را بر میگرداند به همین خاطر از شر ارور های Method Not Mocked خلاص شدیم .هشدار: راه حل بالا اگر چه بسیار کار ساز است اما باید توجه داشت که به عنوان آخرین روش مورد استفاده قرار گیرد .زیرا ممکن است باعث pass شدن تست های مشکل دار شود .پاور ماک (Power-Mock) چیست ؟پاور ماک یک لایبرری قدرتمند برای ماک کردن است که از امکان رفلکشن استفاده میکند .اصولا Mockito توانایی ماک کردن static ها ، final ، private ، و کانسترکتور ها در کلاس ها  و متد ها را ندارد برای ماک کردن این نوع از کلاس ها از پاور ماکیتو استفاده کنید .در مواردی که در unit test به بن بست میرسید استفاده از این ابزار را فراموش نکنید.ارور unnecessary stubbing exception چیست ؟این ارور نیز یکی دیگر از ارور های رایج در ماکیتو است تصور کنید تستی را می نویسید ، آن را اجرا می کنید . همه چیز بدون مشکل است و تست شما پاس می شود .اما به محض آن که همه ی تست های موجود در تست کلاس خود را اجرا می کنید علارغم پاس شدن همه ی تست ها با این ارور نیز مواجه میشوید .هر گاه برای یک ابجک ماک شده شرایطی را تعریف کنیم (استفاده از Mockito.when)در چند مورد با این ارور مواجه میشویم:1) شرایط (when) مورد استفاده قرار نگیرد.این کد ، کد مرده حساب میشود و طبق دایکیومنت mockito به جهت جلوگیری از سر بار باید حذف شود2) منطق اشتباهی در حال mock شدن است { در منطق تجدید نظر شود }3) قبل از اجرا شدن when تست fail میشوداین ارور از ماکیتو ورژن 2 به بعد به جهت1) Detect unused stubs in the test code 2) Reduce test code duplication and unnecessary test code3) Promote cleaner tests by removing ‘dead&#x27; code4) Help improve debuggability and productivityداده میشود.به منظور اصلاح ارور ، توسعه دهنده یا باید موارد شامل ارور را مرتفع سازد یا آن هارا نادیده بگیرد .به منظور نادیده گرفتن یک شرط میتوان از کد زیر استفاده کرد . که به آن خنثی سازی ملایم نیز گفته میشود
Mockito.lenient().when()اما برای سرکوب تمام exception ها میتوان از کد زیر در بالای تست کلاس استفاده کرد
@RunWith(MockitoJUnitRunner.Silent.class) اما طبق داکیومنت ماکیتو این روش توصیه شده نیست و توسعه دهنده باید موارد شامل ارور را بر طرف کند. نکته : در بسیاری از موارد با حذف کردن یکی از شرایط (Mockito.when) که در تست کاربردی ندارد میتوان این ارور را برطرف کرددر این جا ما با مهم ترین ابزار ها و چالش های ماک کردن آشنا شدیم . در انتهای مقاله نیز با مثال هایی از ماکیتو آشنا خواهیم شد.اکنون به سراغ اصل سوم از فرمولمان می رویم.عملکرد کد را به لحاظ آن چه مورد انتظار است بررسی کنید (کاری به implementation نداشته باشید)این اصل به ما میگوید که برای تست نویسی هدف مهم است نه مسیر !با یک مثال توضیح میدهیم :فرض کنید که متدی دارید که دو عدد را دریافت میکند و حاصل جمع آن ها را محاسبه میکند در این جا چه چیزی باید تست شود ؟ما عملکرد متد را به ازای ورود چند عدد مختلف تست میکنیم مشخصا این متد باید خروجی های مورد انتظار ما را حاصل کند .اما این که این متد چگونه و با چه روشی عمل جمع را انجام میدهد برای ما اهمیتی ندارد (در واقع به پیاده سازی متد کاری نداریم )رعایت این نکته باعث میشود که تست های نوشته شده اصولی باشند و درگیر جزئیات پیاده سازی نشوند.چه طور میتوان خروجی حاصل شده و خروجی مورد انتظار یک متد را بررسی کرد؟با Assertation ها میتوان به این امکان دست یافت ، Assert ها کد هایی executable هستند که پاس شدن یا فیل شدن تست های ما به آن ها وابسته است .این نکته نیز حائز اهمیت است گاهی از اوقات آن چه از یک متد انتظار می رود یک خروجی مشخص نیست بلکه انجام کاری (مثلا کال کردن یک متد دیگه است) در این موارد به جای assert از متد verify در ماکیتو استفاده میکنیم :Mockito.verify(mockedList,VerificationModeFactory.atLeast(1)).size()در کد بالا مشخص کرده ایم که آیا متد ()size در آبجکت ماک شده ی mockedList حداقل 1 بار ( (1)atLast )کال شده است یا خیر نکته مورد توجه در verify این است که فقط روی آبجکت های ماک شده یا Spy کار می کند . در نهایت نیز به بیان چند نکته ی کاربردی در تست نویسی میپردازیم:1) در یونیت تست ها ابتدا سناریویی که تست در آن fail یا به مشکل میخورد را بررسی کنید .زیرا ممکن است که در ابتدا با pass شدن تست تصور کنید که تست درستی نوشته اید در حالی که تست شما قبل از رسیدن به متدهای assert با شکست مواجه شده است ومتد های assert اجرا نشده اند . در نتیجه تست شما همواره پاس میشود حتی به ازای (1==2)assertدر نتیجه تستی که نوشته اید را از جهات مختلف با مقادیر متفاوت بررسی کنید .(اگر چه ، هر چه در تست نویسی ماهر تر شوید کم تر با این موضوع مواجه میشوید)2) در تست نویسی بن بستی وجود ندارد پس اگر به مشکل خوردید به دنبال راه حل باشید .گاهی اوقات ، راه حل در اصلاح کد های موجود است گاهی اوقات در استفاده ی درست از ابزار ها،یا افزودن یک لایبرری به پروژه گاهی اوقات راه حل در خلاقیت به خرج دادن است یا گاهی اوقات جستجوی بیشتر پس برای خود محدودیتی قائل نشوید .3) بعد از تکمیل کردن تست های یک تست کلاس ، در اندروید استادیو دوبار روی (Ctrl) کلیک کنید و در کادر باز شده یکی از اسکریپت های زیر را وارد کنید :gradlew test  //windows 
./gradlew test  //linuxبعد از اینتر زدن همه ی تست های پروژه شما شروع به اجرا میکند .این در زمانی مناسب است که بخواهید از یک ابزار Ci /Cd استفاده کنید ، در نتیجه مطمئن خواهید شد که تست های شما در سرور های Ci نیز به درستی کار میکنند و pipeline را fail نمیکنند .گاهی اوقات ممکن است که تست های شما در تست کلاس یا ماژول مشخصی درست کار کنند اما وقتی که اسکریپت بالا را اجرا میکنید و همه ی تست ها اجرا میشوند ، ممکن است متوجه مشکلی در یکی از تست های خود شوید . نکته ی بعدی یه نکته خیلی مهم و کاربردیه :?4) میتوانید برای آماده سازی شرایط تست ، در تست کلاس خود کد هایی را بنویسید تا عملیات تست نوشتن را آسان تر کنید.مثل زیر را در نظر بگیرید :class A {
    protected fun init() { } // will be called by internal logic
}همان طور که مشاهده می کنید کلاس A شامل متدی protected به نام init است همان طور که می دانید در کاتلین متد های protected در پکیج های دیگر در دسترس نیستند .بنابراین در تست هایمان با آبجکت گرفتن از A نمیتوانیم به init دسترسی داشته باشیم .راه حل چیست ؟ شاید power Mock بتواند کمک کند ..اما یک راه حل دیگر ساختن کلاسی کمکی است تا با ارث بری کردن از کلاس A به متد init دسترسی داشته باشد تا بتوانیم به جای کلاس A از آن استفاده کنیم .طبق نکته ی گفته شده میتوانیم این کار را در خود تست کلاسمان انجام دهیم:@SmallTest
@RunWith(MockitoJUnitRunner::class)
class BaseRepositoryTest {

@VisibleForTesting
private class AClassAccessor() : A {
    fun getInitMethod() = init()
}

@Test
fun `test`() {
  AClassAccessor().getInitMethod()   //access To init 
}
}همانطور که مشاهده میکنید در تست کلاس، یک کلاس دیگر با نام AClassAccessor ساختیم تا با ارث بری کردن از کلاس A بتواند دسترسی به متد protected را برای ما فراهم کند حالا در تست هایمان میتوانیم از AClassAccessor استفاده کنیم تا با متد getInitMethod به init دسترسی داشته باشیم .در واقع طبق نکته گفته شده با استفاده از چند خط کد توانستیم چالش دسترسی به متد های protected حل کنیم .همچنین میتوانیم مقادیری که تست های ما به آن ها احتیاج دارند را در تست کلاس های خود جنریت کنیم .به طور خلاصه تست کلاس های ما الزاما شامل تست متد ها(متد هایی که با @Test علامت گذاری شده اند) نیستند و می‌توانند حاوی توابع و کلاس های دیگری باشند که به عمل تست نویسی کمک می‌کند .در انتها نیز به بررسی یک مثال خواهیم پرداخت .این مثال از آن جهت انتخاب شده است که شامل دو کامپوننت (live data , coroutines) است تست نویسی برای این دو مورد شامل نکاتی است که آن ها را در این مثال بررسی میکنیم :@HiltViewModel
class LoginViewModel @Inject constructor(val loginUseCase: LoginUseCase) : BaseViewModel() {

private var _liveData: MutableLiveData&lt;LoginResponse&gt; = MutableLiveData()
val liveData = _liveData

fun sendLoginRequest(phoneNumber: String) {
    viewModelScope.launch {
        val result = loginUseCase.run(LoginUseCase.Params(phoneNumber))
        result.onSuccess {
            _liveData.postValue(it.result)
        }
    }
 }
}کد بالا ویو مدلی است که شامل یک تابع برای ارسال درخواست لاگین به سرور میباشدلاگین ویو مدل یک useCase را که وظیفه ارسال درخواست را بر عهده دارد را به عنوان ورودی دریافت میکند .همچنین این ویو مدل شامل لایو دیتایی است نتیجه ی درخواست لاگین در صورت success بودن برای آن فرستاده میشود . اکنون به عملکرد تابع sendLoginRequest میپردازیم:این تابع به عنوان ورودی یک شماره موبایل دریافت میکند .به دلیل آن که تابع run در loginUseCase یک تابع suspend (کروتین) است از بلاک viewModelScope.launch استفاده کردیم .کلاس loginUseCase یک api را کال می کند و درخواست مارا به سرور ارسال میکند و درصورتی که درخواست موفق باشد بلاک success اجرا میشود.در بلاک success مقدار بازگشتی از سرور به لایو دیتا ارسال میشود .حالا به تست این کلاس خواهیم پرداخت:/**
 *  LoginViewModel unit test run on jvm
 */


@SmallTest
@ExperimentalCoroutinesApi
@RunWith(MockitoJUnitRunner::class)
class LoginViewModelTest {

    companion object {
        const val PHONE_VALID_NUMBER = &amp;quot09190000000&amp;quot
    }

    @get:Rule
    var instantTaskExecutorRule = InstantTaskExecutorRule()

    @ExperimentalCoroutinesApi
    @get:Rule
    var coroutineTestRule = CoroutineTestRule()


    lateinit var viewModel: LoginViewModel

    @Mock
    lateinit var loginUseCase: LoginUseCase

    @Before
    fun setup() {
        viewModel = LoginViewModel(loginUseCase)
        runBlockingTest {
               `when`(loginUseCase.run(LoginUseCase.Params(PHONE_VALID_NUMBER))).thenReturn(
                Either.Right(BaseResult(1, &amp;quotmessage&amp;quot, LoginResponse(&amp;quotToken&amp;quot, &amp;quotName&amp;quot)))
            )
        }

@Test
fun `success user login`() {
    runBlockingTest {
        viewModel.sendLoginRequest(PHONE_VALID_NUMBER1)
        val result = viewModel.liveData.getOrAwaitValueTest()
        assert(result.token == &amp;quotToken&amp;quot)

     }
  }
}در پکیج Test(نه androidTest ) کلاس LoginViewModelTest را ایجاد کردیم:انوتیشن SmallTest@ را به توصیه ی گوگل به بالای یونیت تست هامون اضافه کردیم(همچین Medium@ برای تست های instrumentation و Large@ برای ui test ها)  انوتیشن ExperimentalCoroutinesApi@ را نیز برای رفع وارنینگ CoroutinTestRule اضافه کردیم (این Rule بعدا توضیح داده خواهد شد)انوتیشن (MockitoJunitRunner::class)RunWith@ را به جهت مشخص کردن رانر تست ها و initial کردن Mock@ ها اضافه کردیم در این جا برای استفاده از این انوتیشن باید لایبرری ماکیتو را به پروژه اظافه کرده باشیمحالا به قوانین یا همان Rule ها میرسیم :رول اول InstantTaskExecutorRule است ،  از آن استفاده می کنیم تا مطمئن شویم کامپوننت های ما (مثل LiveData) ترد Main را کال نمیکنند . چون در غیر این صورت با ارور زیر مواجه میشویم:java.lang.NullPointerException at androidx.arch.core.executor.DefaultTaskExecutor.isMainThread(DefaultTaskExecutor.java:7)
 at androidx.arch.core.executor.ArchTaskExecutor.isMainThread(ArchTaskExecutor.java:116)
 at androidx.lifecycle.LiveData.assertMainThread(LiveData.java:486)
 at androidx.lifecycle.LiveData.observeForever(LiveData.java:224)همچنین لازم است بدانیم که برای استفاده از InstantTaskExecutorRule باید کتابخانه زیر را به پروژه اظافه کنیم:androidTestImplementation(&amp;quotandroidx.arch.core:core-testing:2.1.0&amp;quot)رول دوم CoroutineTestRule است ، از این قانون زمانی استفاده میکنیم که در پروژه خود از کروتین ها استفاده کرده باشیم .در صورت عدم استفاده از این رول با ارور زیر مواجه میشویم:Exception in thread &amp;quotmain @coroutine#1&amp;quot java.lang.IllegalStateException: Module with the Main dispatcher had failed to initialize. For tests Dispatchers.setMain from kotlinx-coroutines-test module can be usedاین رول به طور پیشفرض وجود ندارد و باید خودمان آن را بنویسیم بنابراین در پکیج تست (پکیجی که یونیت تست های خود را در آن مینویسید)، کلاسی با نام CoroutineTestRule ایجاد میکنیم و کد های زیر را به آن اظافه می کنیم :import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.rules.TestWatcher
import org.junit.runner.Description
import kotlin.coroutines.ContinuationInterceptor

@ExperimentalCoroutinesApi
class CoroutineTestRule : TestWatcher(), TestCoroutineScope by TestCoroutineScope() {

    override fun starting(description: Description) {
        super.starting(description)
        Dispatchers.setMain(this.coroutineContext[ContinuationInterceptor] as CoroutineDispatcher)
    }

    override fun finished(description: Description) {
        super.finished(description)
        Dispatchers.resetMain()
    }

}بعضی از کلاس های استفاده شده در کد بالا از لایبرری coroutines test استفاده میکنند پس نیاز است آن را نیز به گردل سطح ماژول خود اظافه کنید :testImplementation(&amp;quotorg.jetbrains.kotlinx:kotlinx-coroutines-test:x.x.x&amp;quot)
testImplementation(&amp;quotorg.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x&amp;quot)به جای x.x.x ورژن کاتلین کروتین خود را وارد کنید .این لایبرری امکان استفاده از runBlockingTest را نیز میدهد که به آن خواهیم پرداخت .حالا میتوانید از این کلاس به عنوان یک Rule در کلاس های تست خود استفاده کنید .به ادامه ی توضیح تست کلاس خود میپردازیم :به کمک انوتیشن Mock@ یک آبجکت ماک از کلاس loginUseCase ساخته ایم تا آن را به عنوان پارامتر ورودی به ویو مدل پاس بدهیم .حالا به متد setup که با @Before علامت گذاری شده است میرسیم . این متد قبل از اجرای هر تست متد یکبار کال میشود و وظیفه ی کانفیگ چیز هایی را دارد که تست های ما به آن ها نیاز دارند .در این متد ابتدا یک آبجکت برای کلاسی که میخواهیم تست کنیم تهیه کرده ایم و ماک آبجکت loginUseCase  را به آن پاس داده ایم .(ما نمیخواهیم که loginUseCase  یک درخواست واقعی به سرور بفرستد بلکه تنها ورودی و خروجی ها آن برای ما مهم است ، پس آن را ماک کرده ایم)در خط بعد به کمک Mockito.when انتظارات خود از loginUseCase را مشخص کرده ایم ، که به ازای ورود یک شماره تلفن مشخص باید خروجی ای از نوع کلاس Either که شامل یک ریسپانس مشخص است را برگرداند.اما در این جا کاربرد runBlockingTest چیست؟متد run از کلاس loginUseCase یک متد suspend است ، همانطور که میدانید متد های ساسپند در متد های ساسپند دیگر میتوانند کال شوند . اما متد تست ما نمیتواند suspend باشد بنابراین به پلی میان این دو نیاز داریم  runBlocking ها بلاک هایی هستد که میتوانیم کد های ساسپند را در آن ها اجرا کنیم .اما RunBlockingTest یک بلاک ارائه شده در لایبرری coroutines test است که برای تست متد ها کاستومایز شده است .از این بلاک استفاده کردیم تا متد run قابل اجرا باشد .حالا به سراغ تست متد میرویم :چه چیزی را میخواهیم تست کنیم ؟ میخواهیم بدانیم که اگر یوزکیس ما درخواستی را بفرستد خروجی مورد نظر حاصل میشود و آن خروجی به درستی به لایو دیتا  ارسال میشود یا خیر .بنبراین متد sendLoginRequest را با همان شماره تلفنی که برای ماک آبجکت مشخص کردیم فراخوانی میکنیم اکنون لایو دیتا باید خروجی مورد انتظار ما را داشته باشد .پس باید به محتویات آن دسترسی داشته باشیم (آن را observe کنیم )اما observe کردن لایو دیتا یک چالش است ، چرا که ما در محیط تست هستیم و به همان صورت که در کد هایمان عمل میکردیم در این جا امکان پذیر نیست . گوگل با ارائه یک کلاس کمکی این چالش را برطرف کرده است ، بنابراین  یک کاتلین فایل با نام LiveDataUtilTest  بسازید و کد های زیر را به آن اضافه کنید :import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException

/**
 * Gets the value of a [LiveData] or waits for it to have one, with a timeout.
 *
 * Use this extension from host-side (JVM) tests. It&#039;s recommended to use it alongside
 * `InstantTaskExecutorRule` or a similar mechanism to execute tasks synchronously.
 */

@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun &lt;T&gt; LiveData&lt;T&gt;.getOrAwaitValueTest(
    time: Long = 2,
    timeUnit: TimeUnit = TimeUnit.SECONDS,
    afterObserve: () -&gt; Unit = {}
): T {
    var data: T? = null
    val latch = CountDownLatch(1)
    val observer = object : Observer&lt;T&gt; {
        override fun d(o: T?) {
            data = o
            latch.countDown()
            this@getOrAwaitValueTest.removeObserver(this)
        }
    }
    this.observeForever(observer)

    try {
        afterObserve.invoke()

        // Don&#039;t wait indefinitely if the LiveData is not set.
        if (!latch.await(time, timeUnit)) {
            throw TimeoutException(&amp;quotLiveData value was never set.&amp;quot)
        }

    } finally {
        this.removeObserver(observer)
    }

    @Suppress(&amp;quotUNCHECKED_CAST&amp;quot)
    return data as T
}
با کمک متد getOrAwaitValueTest میتوانید هر لایو دیتایی در تست های خود را observe کنید .حالا همانطور که در تست کلاس ، مشاهده می کنید ما لایو دیتا را به کمک این متد observe کرده ایم و خروجی آن که همان کلاس LoginResponse ای است که در ماک آبجکت خود مشخص کردیم را در یک متغیر result ریخته ایم .حالا نوبت بررسی صحت خروجی حاصل شده است . بنابراین توکنی که در ماک آبجکت ، به عنوان خروجی مورد انتظار مشخص کرده بودیم را با توکن حاصل شده در متغیر result را به کمک assert مقایسه کرده ایم .پس زمانی که لایو دیتا داشتیم از  InstantTaskExecutorRule و getOrAwaitValueTest و زمانی که کروتین داشتیم از CoroutineTestRule و runBlockingTest استفاده می کنیم .این تست پاس میشود چون آن چه انتظار داشتیم حاصل شده است.ما در این مقاله با مهم ترین مواردی که برای نوشتن unit test ها به آن احتیاج داشتیم آشنا شدیم و فرمولی برای آن معرفی کردیم اما این تازه شروع ماجرا بود ، بقیه اش تمرین و پیگیری و استمرار است .همانطور که در دبیرستان با حفظ همه ی فرمول های مشتق هم نمیشد به همه ی سوال های آن پاسخ داد و به تلاش و تمرین احتیاج بود .این مقاله همه اش شامل تجربه ، مطالعه و تا حدودی درک شخصی من از مبحث تست نویسی بود بنابراین اگر در جایی از آن ایرادی دیدید ، ممنون میشوم که آن را به من بگویید .همچنین اگر سوالی داشتید همین جا در کامنت ها یا در لینکدین من بپرسید .تا جایی که اطلاع داشته باشم قطعا جواب میدم ?مرسی که تا انتها همراه من بودید:))</description>
                <category>Mohsen Abedini</category>
                <author>Mohsen Abedini</author>
                <pubDate>Fri, 31 Dec 2021 12:31:49 +0330</pubDate>
            </item>
                    <item>
                <title>کرش لیتیکس با اپ متریکا (yandex-AppMetrica)</title>
                <link>https://virgool.io/@mohsenabedini79ooo/%DA%A9%D8%B1%D8%B4-%D9%84%DB%8C%D8%AA%DB%8C%DA%A9%D8%B3-%D8%A8%D8%A7-%D8%A7%D9%BE-%D9%85%D8%AA%D8%B1%DB%8C%DA%A9%D8%A7-yandex-appmetrica-kznl3ztttmmq</link>
                <description>اگر اندروید دولوپر باشید قطعا کرش لیتیکس (crashlytics) به گوشتون خورده یا حتی ازش استفاده کردید و میدونید که چه قدر مهمه که بعد از لانچ شدن محصول ، از کرش های اپ روی گوشی یوزر ها مطلع باشیم .وقتی در مورد کرش لیتیکس صحبت میکنیم اکثرا یاد سرویس معروف فایربیس (firebase) می افتیم اما اگر با فایربیس کار کرده باشید میدونید که اکثر سرویس هاش برای ما تحریمه و همین تحریم ها مشکلات زیادی رو در راه اندازی و استفاده از خدماتش برای ما ایجاد میکنه .شاید براتون پیش اومده باشه که زمان زیادی رو برای کانفیگ مثلا (فایربیس کرش لیتیکس) گذاشته باشید چون که فقط دیوایسی که باهاش تست میکنید vpn درست و حسابی نداشته .یا حتی لاگ کرش ها درست ارسال نشده و مشکلات دیگه ای که در کانفلیکت لایبرری ها ممکنه پیش بیاد.البته در قدرت فایربیس شکی نیست ، اما برای بیزینس های ما احتمالا گزینه ی مطمئنی نیست ...خب حالا چه کار میشه کرد ؟برای ردیابی کرش ها سرویس ها زیادی وجود داره که میشه ازشون استفاده کرد مثل اینستاباگ ،سنتری ، آماروید و...هر کدوم از این سرویس ها مزایا و شاید معایبی داشته باشن که با توجه به بیزینس خودتون باید گزینه مناسب رو انتخاب کنید .در این مقاله من به معرفی App Metrica میپردازم که به نظرم برای کرش لیتیکس گزینه ی مناسب و بی دردسریه.معرفی App Metricaاپ متریکا یه سرویس روسیه (پس احتمال تحریم شدنش کم تره ?) که کار تیم قدرتمند یاندکسه (yandex) که  موتور جستجوی yandex رو توسعه دادن .طبق توضیحات خود سایت :AppMetrica covers the three key features for discovering your app&#x27;s performance - ad tracking, usage analytics and crash analytics.ویژگی های App Metricaاگر بخوام به طور خلاصه ویژگی های این سرویس رو معرفی کنم اپ متریکا رایگانه (نیازی نیست خودتون سرور تهیه کنید)محدودیت نداره (هر تعداد کرش رو بدون محدودیت ریپورت میکنه )به جز کرش لیتیکس قابلیت های زیادی داره (مثل ابزار های انالیز ، کمپین ، فروشگاه و پروفایل یوزر ها )کانفیگ کرش لیتیکسش بسیار راحته و ارور هایی که میده خوانایی بالایی داره و بسیار کمک کنندست مالتی پلتفرمه راه اندازی Crashlyticsخب تا اینجای کار با اپ متریکا  آشنا شدیم حالا بریم سراغ هدف اصلی مقاله یعنی کرش لیتیکساول باید یه اکانت در یاندکس درست کنید . برای ساختن اکانت به این لینک برید .حالا باید پروژتون رو در اپ متریکا ایجاد کنید . برای ساختن پروژتون به این لینک برید .خب اینجا که توضیخ خاصی نداره در صفحه بعد هم ایمیلتون رو وارد کنید تا نوتیف ها برای ایمیلتون ارسال بشه اما صفحه بعدیهمون طور که میبینید یه Api key به ما داد . (حتما کپی کنیدش بعدا لازمش داریم) و پروفایل اپ ما ساخته شد.خب حالا نوبت اضافه کردن لایبرری ها به پروژه اندرویدمونه .کد زیر رو به گردل (سطح پروژه -  build.gradle ) اظافه کنید. dependencies {
 classpath ‘com.yandex.android:appmetrica-build-plugin:0.2.4’
}حالا به سراغ گردل (سطح اپلیکیشن - ماژول app ) میریم .اول پلاگین زیر رو اظافه کنید apply plugin: &#039;appmetrica-plugin&#039;حالا خیلی آروم دیپندنسی رو هم اظافه می کنیم dependencies { 
implementation ‘com.yandex.android:mobmetricalib:3.15.0’ 
}داخل بلاک اندروید کد زیر رو هم اظافه کنید (دیگه تمومه)//groovy

android {   
 appmetrica {  
 postApiKey =&amp;quotPOST_API_KEY&amp;quot    
 mappingBuildTypes = [&#039;release&#039;] 
 }
 } // kotlin

appmetrica  {
   setPostApiKey( &amp;quot Post_Api_Key&amp;quot )
    setMappingBuildTypes(listOf())
}سوال اول : post api key رو از کجا بیاریم ؟در اپ متریکا ما دوتا کلید داریم یکی api key (که هنگام تعریف پروژه بهمون داد و بعدا ازش استفاده میکنیم) یکی هم post api key که باید داخل گردل ازش استفاده کنیم .حالا این کلید ها کجان؟ داخل پروفایل اپتون در اپ متریکا به منوی setting برید اونجا میتونید هر دوتا کلید رو مشاهده کنید .راستی مواظب کلید ها باشید ...حالا در گردل post api key خودتون رو وارد کنید .سوال دوم : mappingBuildTypes چیه ؟اگر که از ابزار هایی مـثل پروگارد استفاده می کنید (در واقع minifyEnabled =true)حتما باید بیلد تایپ رو به mapping BuildTypes بدید . مثل این mappingBuildTypes = [&#x27;release&#x27;] و اگر از پروگارد استفاده نمیکنید نیاز نیست که به mapping BuildTypes  مقدار بدید .مثل این mappingBuildTypes = [&#x27;&#x27;] اما چرا ؟ وقتی که از ابزار هایی مثل پروگارد استفاده میکنید کد اصطلاحا در هم ریخته میشه و هنگامی که کرش اتفاق می افته اپ متریکا احتیاج داره که کد رو مپ کنه تا لاگی که برای شما ارسال میشه خوانا باشه .نکته مهم اینکه اگر پروگارد فعال باشه و یادتون بره mapper رو مقدار دهی کنید اپ متریکا بهتون ارور میده .کار ما با گردل تموم شد حالا پروژه رو سینک کنید .فعال سازی Crashlyticsداخل پروژتون یه کلاس اپلیکیشن تعریف کنید (یادتون باشه که در manifest معرفیش کنید)کد های زیر رو به onCreate کلاس اضافه کنید تا هنگام اجرای برنامه این کد ها هم اجرا بشن override fun onCreate() {
    super.onCreate()

    // init AppMetrics and Activate for Crashlytics

    val config = YandexMetricaConfig.newConfigBuilder(Your_Api_Key)
        .withNativeCrashReporting(false)
        .withLocationTracking(false)
        .withAppVersion(Your_App_version)
        .build()

    YandexMetrica.activate(this, config)
    YandexMetrica.enableActivityAutoTracking(this)

} val config = YandexMetricaConfig.newConfigBuilder(Your_Api_Key)تو این خط باید api key خودتون که هنگام تعریف اپ بهتون داد رو وارد کنید  .withNativeCrashReporting(false)اگر از کد ها نیتیو استفاده می کنید مقدار true و در غیر این صورت false بدید .withLocationTracking(false)اگر که میخواهید از امکان لوکیشن ترک برای مشخص کردن موقعیت مکانی دیوایس هایی که کرش روی اونا اتفاق افتاده استفاده کنید true و در غیر این صورت false بدهید اگر true دادید یادتون باشه که permission location رو هم باید در منیفست تعریف کنید &lt;manifest&gt;
    &lt;uses-permission android:name=&amp;quotandroid.permission.ACCESS_COARSE_LOCATION&amp;quot/&gt;
    &lt;application&gt;...&lt;/application&gt;
&lt;/manifest&gt;.withAppVersion(Your_App_version)ورژن اپ خودتون رو هم اینجا وارد کنید .کار ما تموم شد ...حالا اپ خودتون رو اجرا کنید ...لاگ برنامه رو چک کنید اگر با اروری مواجه نشدید یعنی کارتون رو درست انجام دادید .اگر api key یا post api key رو اشتباه وارد کنیم چی میشه ؟(اتفاق های خیلی بد .  بدجورررررررررر ?)اگر کلید هارو اشتباه یا جا به جا وارد کنید حین اجرای برنامه بهتون ارور میده و باید اصلاحشون کنید .از کجا مطمئن بشیم که درست کار میکنه ؟خیلی راحته یه اکسپشن در برنامه ایجاد کنید تا حین اجرا کرش کنه بعد پروفایل اپتون رو چک کنید تا ببینید کرش ریپورت میشه یا نه راستی یه نکته مهم اینکه بعضی وقت ها یه مدتی طول میکشه تا کرش ها ریپورت بشه پس عجله نکنید ..امیدوارم که این مقاله براتون مفید بوده باشه . سعی کردم بیش تر نکاتی که لازمه رو بگم ، اگر شما هم دیتایی داشتید که نگفته بودم تو کامنت برامون بنویسید .</description>
                <category>Mohsen Abedini</category>
                <author>Mohsen Abedini</author>
                <pubDate>Fri, 12 Nov 2021 16:21:08 +0330</pubDate>
            </item>
            </channel>
</rss>