Gradle: مرد هزار چهره (Product Flavors)

به تازگی مجبور شدم از یک پروژه خیلی قدیمی ۲ نسخه جدید ریلیز کنم. کاری که برای من پیچیدگی نگه‌داری داری زیادی داره. کدهای هر نسخه با نسخه دیگه متفاوته. اما این تفاوت اونقدر نیست که بشه ازش به عنوان یه پروژه کاملن جدا اسم برد. از طرفی جدا نکردن پروژه‌ها از هم باعث می‌شه وارد فضای کثیف کامنت اسپاگتی/کد اسپاگتی بشم. در بخش‌هایی برام مهمه که تغییراتی که تو یکی از ریلیزها می‌دم، تو بقیه اعمال نشه و…! بهترین راهی که به نظرم رسید اینه که از product flavors استفاده کنم.

پیش‌نوشت: این قابلیت جذاب گریدل من رو یاد اکبرعبدی انداخت. مرد هزار چهره سینمای ایران که به هر شکل وشمایلی در میاد و هر کاراکتری رو بازی می‌کنه. یه جورایی گریدل هم داره همین کار می‌کنه. هر جوری که بهش بگید بر اساس شرایط و ضوابطی که مشخص کنید براتون متفاوت بازی می‌کنه.

به طور پیش‌فرض وقتی یک ماژول جدید می‌سازیم، اندروید استودیو release build type و debug build type رو می‌سازه و برنامه در حالت دیباگ کامپایل می‌شه. خب این build type مشخص می‌کنه که در هر حالت برنامه به چه شکل و طبق چه قواعدی کامپایل بشه. مثلن ساین فایل APK بنا به اینکه در کدوم حالت باشه فرق بکنه. یا مثلن درخواست‌های HTTP رو در حالت دیباگ لاگ کنه و در حالت ریلیز نه.

https://gist.github.com/Ssisakhti/a5d7106b0e260ece1f48e4fad88c8f85

اولین چیزی که به نظرم می‌رسه اینه که ApplicationId این فایل‌ها باید با هم متفاوت باشند. پس یه ApplicationId رو به عنوان پایه قرار می‌دم:

https://gist.github.com/Ssisakhti/22c4f17906a307b29f12730c43d2e283

حالا اگر برنامه رو در دیباگ مود کامپایل کنید application package name شما me.sisakhti.debug خواهد بود و اگر برنامه رو در ریلیز مود کامپایل کنید application package name برنامه me.sisakhti خواهد بود.

همونطور که می‌تونین حدس بزنید آپشن‌های زیادی در مقابل داریم. یک نگاهی به این لینک بندازید:

BuildType
 DSL object to configure build types

تو این بخش مواردی مثل applicationIdSuffix یا versionNameSuffix رو می‌تونیم کنترل کنیم.

حالا داستان product flavor چیه این وسط؟ product flavor فراتر از یک بیلدتایپ ساده است. اگر نگاهی به properties و methods و scripts هرکدوم بهتر متوجه می‌شیم.

Product Flavor properties, methods, scripts

و

Build Type properties, methods, scripts

بیشتر مثال‌ها در این زمینه شامل سناریو اپ‌هایی هستند که دو نسخه Free و Paid دارند. من ترجیح می‌دم مثالی که مجبور شدم روش کار کنم رو مطرح کنم. سال‌های دور اپی توسعه داده شده به اسم Chikija. حالا من می‌خوام علاوه بر اینکه اون رو به روزرسانی کنم اپ دیگری رو شبیه همون با تغییر در ظاهر و عملکرد اما بر همون پایه بسازم. بیاید فرض کنیم اپ دوم اسمش Filmbazi باشه.

جذابیت قضیه اینه که همه چیز خیلی ساده است. برای تعیین application package name هر flavor و اسم ورژنش همچین چیزی رو به فایل گریدل پروژه اضافه می‌کنیم:

https://gist.github.com/Ssisakhti/facdc77a799914b3cddd435ee4ef9fcf

حالا پروژه رو سینک کنید. احتمالن با همچین اروری مواجه می‌شید

Error:All flavors must now belong to a named flavor dimension
The flavor 'flavor_name' is not assigned to a flavor dimension

اندروید پلاگین ۳ به بعد مکانیزم اتوماتیکی داره برای مچ کردن دپندنسی‌ها و حالت‌های بیلد. برای اینکه همه چیز درست کار کنه باید flavor dimensions رو برای product flavors هم تعریف کنید. اگر فقط یک dimension دارید یا حوصله ندارید بدونید چیه و به چه کار میاد و فقط می‌خواید ارور رو رفع کنید خط زیر رو به فایل gradle اضافه کنید:

https://gist.github.com/Ssisakhti/b0b4f3bef8d3abc6310d92c8a24048ff

من می‌خوام اپ‌ها رو علاوه بر اینکه در کافه‌بازار منتشر می‌کنم، در ویترین ایرانسل هم که خدمات ارزش افزوده هم می‌ده به شکلی منتشر کنم. برای این کار یک Dimension دیگه هم اضافه می‌کنم:

https://gist.github.com/Ssisakhti/3460ff2c794d6b69613d0dab4aaba9d2

پس Build Variants من چیزی شبیه به این می‌شه:

یکی از نکات مهم اینه که به ازای هر بیلد برای استفاده از امکانات Firebase قبلن باید همچین مدلی رو استفاده می‌کردیم:

app/src/

   main/google-services.json

   chikija/google-services.json

   filmbazi/google-services.json

ولی الان کافیه یک پروژه در کنسول فایربیس بسازیم و تمام پکیج‌نیم اپ‌هامون رو بهش اضافه کنیم و فایل google-services.json رو که شامل اطلاعات مورد نیاز برای همه بیلدهاست رو در پوشه app قرار بدیم. گریدل بقیه کارها رو خودش انجام می‌ده.

  • main

->java

->/*Java class files*/

->res

->layout

->mipmap(s)

->values

به ازای هر flavor هم همچین ساختاری خواهید داشت(بنا به نیاز باید بسازید):

  • chikija

->java

->/*admin specific Java class files, if any*/

->res

->layout

->mipmap(s)

->values


  • filmbazi

->java

->/*visitor specific Java class files, if any*/

->res

->layout

->mipmap(s)

->values

برای دسترسی راحت می‌تونید در پنجره Project، حالت نمایش رو از اندروید به Project تغییر بدید.

یا یک فایل xml در ریسورهای پیش‌فرض اپ شما اگر به این شکل باشه:

https://gist.github.com/Ssisakhti/4f2381646a218742e1342aa006a5a074

به ازای هر flavor هم می‌تونید همچین چیزی داشته باشید:

https://gist.github.com/Ssisakhti/370827ce2c4b1290af4a79b97d46199f

زمانی که پروژه با موفقیت Build بشه، gradle برای هر Build Variant تعریف شده، یک فایل به شکل جداگانه می‌سازه به اسم BuildConfig که شامل اطلاعات خاص مربوط به اون Build Variant است. به عنوان نمونه فایل BuildConfig که متعلق به filmbaziCafebazaarDebug Build Variant است:

https://gist.github.com/Ssisakhti/8980d6cd9e3438ebb879c01089f2d7fe

حالا می‌تونیم به شکل زیر رفتار برنامه رو بر اساس هر بیلد کنترل کنیم:

https://gist.github.com/Ssisakhti/c406a0b0986875bdb0fc78abd3af05b9

همچنین می‌تونیم متغیرهای جدیدی برای BuildConfig تعریف کنیم:

https://gist.github.com/Ssisakhti/2ebca3d0e5734f3b0f19dc1ce0852d00

اگر تغییرات ریسورس‌ها خیلی کمه و حوصله هندل کردن چند فایل جدید رو ندارید می‌تونید در فایل build.gradle تغییرات رو تنظیم کنید:

https://gist.github.com/Ssisakhti/40e884b642ab62b49e384fb2edb0e855