سید عماد رضوی
سید عماد رضوی
خواندن ۸ دقیقه·۷ ماه پیش

گذار آرام از Android و JVM به KMP: یک سفر سه ساله

در این مقاله سعی دارم مسیر پروژه‌ای را شرح دهم که بعد از سه سال گسترش دادیم و در نهایت به KMP تبدیل شد. یکی از دلایل من برای انتشار این مقاله نسبتا طولانی این است که اگر برنامه‌نویسی خود را در بخشی از این مسیر می‌بیند، بتواند برای ادامه مسیر پروژه خود به درک بهتری برسد. به جهت برخی ملاحظات ضروری، امکان ارائه مثال‌های واقعی (و تشریح کسب‌و‌کار پروژه) را ندارم. اما تلاش می‌کنم این موضوع مانعی برای شفافیت توضیحات فنی من نباشد. این نوشتار مبتنی بر تجربیات من است، ممکن است در نگارش بخش‌های کوچکی از آن دچار اشتباه شده باشم. امیدوارم با بازخورد سازنده خود، این متن را به سطح کیفی بالاتری برسانید.


می ۲۰۱۹

در ابتدای همکاری من، سه پروژه‌ی اندرویدی که از نظر معماری در شرایط مناسبی نبود، برای رفع نیازهای شرکت ساخته شده بود که همگی از نظر کسب‌و‌کاری اشتراکات زیادی داشتند (مثلا دیتابیس همگی تا حد زیادی شبیه به هم بود). اولین قدم من در هنگام ورود، استفاده از Build Variant ها بود. با این کار، باگ‌های زیادی به وجود آمد و منجر به تغییرات فایل زیادی شد ولی بالاخره هر سه پروژه به پایداری متوسطی رسید. توسعه گسترش پیدا کرد، ظاهر برنامه بهبود را بهبود دادیم. از Volley به Retrofit مهاجرت کردیم. در این زمان، هنوز پروژه ما DI (تزریق وابستگی) نداشت. تمام کلاس‌های سورس با جاوا نوشته شده بود. تمام ظاهر برنامه با XML پیاده سازی شده است. در برنامه هیچ تستی نوشته نشده است و برای برخی نیازهای بیزینسی، ماژول‌های Java ای ساخته بودیم. مثلا ماژول common که شامل Utility های کاربردی برنامه بود.


فوریه ۲۰۲۰

کمی پیش رفتیم. پروژه با پیاده‌سازی بخش‌های گسترده‌ای از کسب‌وکار جلو رفته بود. من اقدام به تبدیل پروژه به Single Activity کردم. به سراغ Fragment و Navigation Component رفتیم. کمی بعد، ما بخشی از Custom View های مربوط به پروژه را به یک ماژول جداگانه اندرویدی منتقل کردیم. هدف ما این بود که هر چه زودتر دیزاین سیستم طراحی شده را وارد پروژه کنیم. هدفی که تا زمان استقرار Jetpack Compose و جایگزینی آن با XML محقق نشد.


می ۲۰۲۰

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


مارس ۲۰۲۱

به ما خبر رسیده است که قرار است یک نسخه JVM از این پروژه برای سیستم‌عامل Linux داشته باشیم. با اضافه شدن آقای محمد سهرابی به پروژه، تمرکز بر روی بحث معماری پروژه افزایش پیدا می‌کند. بالاخره معماری پروژه به شکل فعلی آن یعنی Clean Architecture + MVVM + DI رسید. با تمرکز بیشتر بر روی جنبه‌های معماری پروژه، کم‌کم کلاس‌ها، پوشه‌ها و ماژول‌های قابل توجهی ساخته، تغییر کرده یا حذف شدند. تبدیل فایل‌های جاوا به کاتلین نیز سرعت گرفت. Kotlin Coroutines وارد ماجرا شد. همینطور Hilt Dependency Injection به پروژه اضافه شد.


آگوست ۲۰۲۱

پس از اینکه بخش‌هایی از پروژه اندرویدی به شکل Abstraction/Implementation پیاده‌سازی شد، زمان ساخت یک پروژه JVM فرا رسید. در این زمان ما پیاده‌سازی ارتباط با دیتابیس را در پروژه اندرویدی خود با Room انجام داده بودیم (که تا لحظه نگارش این متن، پشتیبانی از سایر پتلفرم‌ها ندارد) و برای JVM، کتابخانه Exposed را انتخاب کردیم. این کتابخانه از اندروید پشتیبانی نمی‌کرد. گزینه بهتر SQLDelight بود که پلتفرم‌های هدف ما را به خوبی پشتیبانی می‌کرد. ما در آن زمان به این موضوع واقف نبودیم. برای Dependency Injection در اندروید از Hilt استفاده می‌کردیم که در JVM پشتیبانی نمی‌شد. در JVM به سراغ Dagger رفتیم. برای ارتباط با سرور نیز به سراغ کتابخانه قدرتمند Ktor رفتیم چون Retrofit قابلیت استفاده در پلتفرمی غیر از اندروید نداشت. برای اینکه بتوانیم از ماژول‌های مشترک بین دو پروژه بهره ببریم، به سراغ Maven Publish و Gitlab Package Registry رفتیم. کمی جلوتر، مثلا ماژولی برای تمامی ارتباطات با سرور بر اساس Ktor شکل گرفت تا نیازی به نوشتن جداگانه هر بخش از این کدها نباشد. برای Logging به سراغ کتابخانه‌های ساخته شده بر پایه‌ی SLF4J رفتیم. در سمت JVM از Log4j بهره بردیم و در سمت اندروید، مهاجرت به Logback را شروع کردیم.


دسامبر ۲۰۲۱

یک نیروی مستعد به مجموعه اضافه کردیم. آقای مهدی بهمن‌پور با روحیه هنری و فهم خیلی خوب از برنامه‌نویسی، به ما این امید رو داد که در سمت ظاهر برنامه، می‌توانیم انتظار کارهای خیلی جذابی داشته باشیم. در این مدت ما بر روی دامنه برنامه تغییرات زیادی داشتیم و در سمت JVM، کتابخانه Picocli را برای بهبود رابط کاربری CLI مستقر کردیم. در بخش دیتابیس، با درک جدیدی که از کتابخانه‌ Exposed پیدا کردیم، در پیاده‌سازی‌های خود و نحوه نوشتن Query های خود، بهبودهایی داشتیم. مهدی در پیاده‌سازی بعضی بخش‌های مربوط به Gradle ابتکار عمل را در دست گرفت. مثلا ساخت یک اسکریپت برای مشخص ساختن ورژن Debug و Release در پروژه JVM یا تبدیل تمامی فایل‌های Groovy به Kotlin (فرمت kts).


جولای ۲۰۲۲

کم کم پروژه از نظر بیزینسی به ثبات می‌رسد و ما توانستیم زمان و تمرکز بیشتری برای تسک‌های فنی پیدا کنم. بالاخره اولین کدهای Jetpack Compose توسط مهدی به پروژه اضافه شد. بیزینس توسط محمد جلو رفت و سمت ظاهر توسط مهدی راهبری شد. من هم راهبری کلی پروژه را به عهده داشتم و سعی می‌کردم ایده تبدیل پروژه‌ها به KMP را همیشه در ذهن سایر نفرات تیمم زنده نگه دارم.


نوامبر ۲۰۲۲

پروژه را به Version Catalog منتقل کردیم. تغییری که کار ما برای سایر تغییرات در Gradle را راحت‌تر کرد.


آوریل ۲۰۲۳

در این زمان به این نتیجه رسیدیم که نوبت خداحافظی با کتابخانه قدرتمند Room و مهاجرت به SQLDelight در اندروید و JVM شده است. برای اینکه این تغییر با حداقل مشکل انجام شود، محمد سهرابی ماموریتی برای نوشتن تست در لایه دیتا (مخصوصا لایه Datasource) را شروع کرد. با پایان فرآیند تست‌نویسی او، خودش و من مشغول نوشتن فایل‌های sq و اجرای سایر تغییرات شدیم. پس از مدتی، به جهت تفاوت‌های Dagger با Hilt و نیز تفاوت Driver ‌های SQLDelight پیدا شد که نیاز به تغییراتی در نحوه اجرای تست‌ها و تغییر کدهایی در آن قسمت گشت. در مجموع این مهاجرت تکنولوژی به دلیل تفاوت پارادایم میان Room و SQLDelight، زمان قابل توجهی از ما گرفت. اما پس از آن، برای هر دو پروژه اندروید و JVM، یک ماژول مشترک برای ارتباط با دیتابیس برنامه داشتیم.


ژوئن ۲۰۲۳

مهدی بهمن‌پور مسئول پیگیری تسکی شد که ما در آن زمان به اشتباه به آن Feature Flag می‌گفتیم. هدف از این تسک آن بود که در لایه Business هر یک از سه Flavor ای که برای پروژه اندرویدی داشتیم، به جای آنکه بر اساس Flavor حالت مشخصی در نظر بگیریم، Feature های مشترکی را شناسایی کنیم که با تغییر هر یک از آن‌ها در جایی مشخص (مثلا یک فایل که در build.gradle به آن اشاره می‌شود)، بتوانیم ویژگی جدیدی را برای آن Flavor اعمال کنیم. برای این تغییر نیاز بود تا مهدی بخش‌های مختلف برنامه را از نظر امکانات جداسازی کند و برای هر یک از ترکیب حالاتی که ممکن بود پیش بیاید، پیاده‌سازی‌های مناسبی داشته باشد. این کار در نهایت به قابلیت کنترل بیشتر امکانات هر یک از Flavor‌ های برنامه اندرویدی انجامید. وجود یا عدم وجود هر یک از Feature های تعریف شده، به یک Use Case تبدیل شد. تمرکز بیزینس را از Flavor خارج کرد و به ما انعطاف بیشتری برای توسعه پروژه برای شرکت‌های متفاوت با شرایط بیزینسی جدیدی را داد.


جولای ۲۰۲۳

چهارمین Flavor به پروژه اندرویدی اضافه شد. قبل از آن، نیاز بود تا یک Feature تقسیم به دو Feature شود. به جهت پیاده‌سازی انجام شده پیشین، زمان اضافه شدن این Flavor و گرفتن خروجی مناسب آن بسیار کوتاه بود. کمی پیشتر با ناراحتی از محمد سهرابی خداحافظی کردیم و مدتی بعد با اضافه شدن آقای محسن عابدینی به ادامه مسیر قبلی خود امیدوار شدیم. مسیری که می‌توانیم از ظرفیت توجه بالای او به جزئیات، مطالعه مستندات به شکل دقیق و علاقه شدیدش به فهم جدیدترین توسعه‌هایی که در دنیای اندروید اتفاق می افتد، استفاده کنیم.

مارس ۲۰۲۴

به سراغ قدم نهایی رفتیم. در ابتدا تصور ما این بود که KSP با Build Flavor ها به درستی کار نمی‌کند و باید برای پیاده‌سازی چیزی جز آن، زمان قابل توجهی صرف کنیم. همینطور برداشت اشتباه دیگری داشتیم و آن این بود که اگر پروژه اندرویدی با Hilt پیاده‌سازی شده است، امکان استفاده از آن در ماژول مربوط به اندروید وجود ندارد و باید حتما به کتابخانه دیگری تبدیل شود. برای این منظور به سراغ Kotlin Inject رفتم و تبدیل پروژه JVM از Dagger به KI را انجام دادم. آقای عابدینی به سراغ پروژه اندرویدی رفت و آن تبدیل را پیگیری کرد. با این حال این تبدیل (که فعلا نیازی به انجامش نداشتیم)، به ما به مشترک‌سازی بخش بیشتری از پروژه‌ها در قسمت DI انجامید. همزمان ابتدا تمامی ماژول‌های Java/Kotlin و سپس تمامی Android Library ها به پروژه JVM منتقل شد.

در نهایت ساختار پروژه KSP با اضافه شدن یک ماژول از نوع Kotlin Multiplatform به نام shared شکل گرفت و وابستگی به پروژه JVM داده شد. برای پروژه اندرویدی، وابستگی مخصوص پلتفرمی در ماژول shared تعریف نشد و پروژه اندرویدی صرفا به بخش common این ماژول وابسته شد. با اجرای پروژه در هر یک از دو پلتفرم، خیالمان راحت شد که بالاخره هر چند جزئی، یک پروژه KMP داریم که باید تلاش کنیم بهبودش بدهیم.


نکات پایانی

۱. از ابتدا اصلی‌ترین هدف فنی ما، ادغام تمامی پروژه‌ها بود. ولی بیشتر زمان ما صرف پیاده‌سازی نیازهای کسب‌وکار می‌شد. زمانی که شاید اگر کمی زودتر به این هدف فنی رسیده‌ بودیم، با اتلاف زمان کمتری مواجه می‌شدیم. با توجه به پایداری KMP، به نظرم بهترین زمان برای یکی کردن پروژه‌ها، همین الان باشد.

۲. تیم پروژه Room در حال توسعه تسک‌هایی است تا بتواند به صورت یک کتابخانه KMP منتشر شود.

۳. من، محمد سهرابی، مهدی بهمن‌پور و در نهایت محسن عابدینی را می‌توانید در لینکدین پیدا کنید.

۴. من هر هفته برای روزهای پنجشنبه تعدادی جلسه رایگان نیم ساعته در ADPList تنظیم کرده‌ام که در صورت نیاز می‌توانید قرار جلسه‌ای تنظیم نمایید.

jetpack composekmpsqldelightkotlinandroid
یک عدد برنامه نویس خوشحال هستم.
شاید از این پست‌ها خوشتان بیاید