انتقال برنامه اندروید واقعی به چند پلتفرم Kotlin 3/4 — به اشتراک گذاری UI بدون از دست دادن Safety Net

مهاجرت یک برنامه اندروید واقعی به چند پلتفرم Kotlin
قسمت 3: به اشتراک گذاری رابط کاربری بدون از دست دادن شبکه امنیتی
قسمت 2 به یک الگو ختم شد: کدی که سادهترین کار را اجرا میکرد، کدی بود که چیزی درباره اندروید نمیدانست. با این منطق، لایه رابط کاربری باید بدترین بخش مهاجرت باشد. صفحهنمایش، نوارهای سیستم، صفحهکلید، ژستهای پشتی را لمس میکند— همه چیزهایی که به طور غیرقابل انکار پلتفرم هستند.
و در کمال تعجب من اینطور نبود. آهنگسازان به آسانی مدل های دامنه حرکت کردند.
چیزی که تکان نمی خورد همه چیز اطرافشان بود. سیستم منبع، افزونههای ساخت، ابزارهای پیشنمایش، و آزمایشهای اسکرین شات با این فرض ساخته شدهاند که یک ماژول Compose یک ماژول کتابخانه Android است. هنگامی که این واقعیت متوقف شد، کد برنامه خوب بود و اکوسیستم اطراف آن خوب نبود.
به اشتراک گذاری کد یک برنامه ساده تر از اشتراک گذاری اکوسیستم اطراف آن بود. این کل مقاله است.
چیزی که به راحتی حرکت کرد
اکثر طرحبندیها بدون تغییر به CommonMain منتقل شدند. چیدمان، حالت، طرح زمینه، انیمیشن — Compose Multiplatform سطح را پوشش می دهد BikeHub استفاده می شود، و تفاوت برای یک صفحه نمایش معمولی عمدتا وارداتی بود.
دو چیز باعث شد این کار راحتتر از آنچه در غیر این صورت انجام میشد پیش برود.
لایه UDF ابتدا رفت. هر ماژول ویژگی در BikeHub از Ouroboros-Compose، یک کتابخانه جریان داده یک طرفه کوچک استفاده میکند (تحت تاثیر Spotify’s Mobius، که من زمانی از آن استفاده میکردم که نماها و قطعات بر رابط کاربری اندروید حاکم بودند). ViewModel را انتزاع می کند و منطق UI را در کلاس های ساده و قابل آزمایش نگه می دارد. من قبل از لمس یک ماژول ویژگی، آن را به عنوان مرحله 0 کل انتقال به KMP تغییر دادم.
این دستور بیشتر از چیزی که به نظر می رسد معنی داشت. از آنجایی که منطق UI در ViewModel Android وجود ندارد، هیچ وابستگی به androidx.lifecycle وجود نداشت که از طریق 38 ماژول تابعی که منتظر حذف بودند، منتقل شد. دارندگان ایالت اکنون فقط کاتلین بودند.
کد پلتفرم اندروید در لایه UI قبلاً یک بوی کد بود. من سال ها با آن این گونه رفتار کرده بودم، بنابراین چیز زیادی برای یافتن وجود نداشت. برخی از تماسهای LocalContext.current وجود داشتند که به راحتی قابل انتزاع بودند، و تعداد زیادی منابع متنی و طراحی.
نتیجه در طرح ماژول قابل مشاهده است. از بین همه ماژول های تابع، بیشتر آنها دقیقاً یک فایل خارج از CommonMain دارند و آن یک فایل پیش نمایش است. فقط دو مورد چیزی شبیه کد پلتفرم واقعی دارند: یکی نیاز به PlatformWebView دارد و دیگری باید بداند آیا تنظیمات اعلان حتی در پلتفرم واقعی وجود دارد یا خیر.
اگر میخواهید پیشبینیکنندهای از دردناک بودن مهاجرت رابط کاربری خود داشته باشید، احتمالاً این است: چقدر Android لایه ارائه غیرنوشتن شما را لمس میکند.
آنچه در واقع در androidMain وجود دارد
قسمت 1 androidMain را 12 درصد از کد Kotlin قرار می دهد، و من برای مدتی این رقم را به عنوان “بخش مخصوص اندروید برنامه” ذکر کردم.
این نیست از خط 13,843 در androidMain، 9653 ردیف — 70% — هستند ویژگی های @Preview. هفتاد و چهار فایلی که وظیفه کلی آنها رندر کردن یک Composable در پانل پیش نمایش Android Studio و تقویت تست های اسکرین شات است.
آنها را بردارید، و کد واقعاً مخصوص پلتفرم اندروید حدود 4190 خط است که کمتر از 4٪ از پایه کد است.
این کل مهاجرت را برای من تغییر می دهد. چیزی که من آن را «کد پلتفرم» مینامیدم، عمدتاً داربستسازی ابزار بود – قیمت توانایی نگاهی بینداز در رابط کاربری یک پلتفرم، نه هزینه در حال اجرا آن را در سه.
داشتن پیش نمایش ها در androidMain به هیچ وجه فسیلی از یک تجربه رها شده است. حاشیهنویسی @ComponentPreview مورد انتظار/واقعی است، بنابراین توابع پیشنمایش میتوانند در CommonMain وجود داشته باشند—اما آنها برای @Preview در Android نوشته شدهاند، زیرا طرح اولیه این بود که آزمایشهای اسکرینشات از آنها با Compose Preview Screenshot Testing انجام شود. این طرح از مهاجرت جان سالم به در نبرد و تجسم ها در همان جا باقی ماندند.
روبراتزی انتخاب اول نبود. Compose Preview Screenshot Testing گزینه جذابتری روی کاغذ است: شما پیشنمایشهایی را مینویسید که به هر حال مینویسید، و ابزارها آنها را ارائه میکنند. من آن را امتحان کردم و در ماژول کاتلین چند پلتفرم کار نمی کند. بنابراین من به Roborazzi در حال اجرا تحت Robolectric در مجموعه منبع androidHostTest که هدف اندروید KMP از قبل ارائه کرده است، برگشتم.
پیشنمایشهایی که برای رویکرد اول نوشته بودم هنوز وجود دارند— 9653 خط از آنها——اکنون به جای اجرای آزمایشی، پنل پیشنمایش Android Studio را تقویت میکنند.
چیزی که حرکت نکرد: پلتفرم واقعی در لایه UI میپیچد
تصاویر را حذف کنید و آنچه باقی می ماند یک لیست کوتاه و خاص است. همه اینها در core:designsystem و core:ui زندگی می کنند، پشت آنچه انتظار می رود/واقعی:
- پشتیبان — حرکت برگشت در Android، ایده ای متفاوت در iOS و تاریخچه مرورگر در وب.
- FullScreenDialog — منظور از «تمام صفحه» توسط میزبان تعیین میشود.
- اندازه ظرف — چگونه پنجره موجود اندازه گیری می شود.
- PhotoPager و ZoomAsyncImage - زوم کنید و عکسها را ورق بزنید.
- Photo Handler و چندین پسوند اصلاح کننده در core:ui.
- PlatformWebView در یک ماژول تابع، زیرا نمای وب تعبیه شده چهار چیز متفاوت است.
هر چیزی که از کل برنامه خارج میشود——مرورگر، فهرست فروشگاه، تنظیمات اعلان سیستم عامل———از طریق یک رابط ExternalNavigator با پیادهسازی یک پلتفرم میگذرد، بنابراین هیچ ماژول عملکردی هرگز آن را لمس نمیکند.
و سپس خود حاشیه نویسی پیش نمایش وجود دارد:
https://medium.com/media/3f68a9be3ce3be24510ac4cd08cd6aed/href
یک جفت مورد انتظار/واقعی که تنها هدف آن این است که به ابزارهای روی یک پلتفرم اجازه دهند کد را ببینند و آن حاشیه نویسی را در هر جای دیگر بی ضرر نشان دهند. این یک چیز کوچک است، و واضحترین تصویر از آنچه این مقاله در مورد آن است: رابط کاربری جابجا شد، ابزارهای اطراف آن به یک آداپتور نیاز داشتند.
منابع: یک ماژول، دو راه حل
همه منابع رشته ها، نقشه ها، فونت ها — در یک ماژول core:res تحت commonMain/composeResources زندگی می کنند. یک ماژول، یک کلاس Res تولید شده، هر ماژول دیگری به آن بستگی دارد.
مزایای تمرکز آشکار است: یک مکان برای نگاه کردن، بدون کلیدهای تکراری، بدون بسته بندی منابع ماژول پیکربندی. هزینه به همان اندازه واضح است — core:res یک وابستگی به تقریباً همه چیز است، بنابراین ضربه زدن روی یک رشته مقدار زیادی را بازیابی می کند.
چیزی که من انتظار نداشتم نیاز به یک راه حل بازتابی برای قابل استفاده کردن آن در پایین دست بود. افزونه Compose Multiplatform Resources دایرکتوری منبع وظیفه کپی دارایی Android خود را به یک ماژول کتابخانه متصل نمی کند، و نوع کار داخلی است — بنابراین هیچ API برای تنظیم آن تایپ نشده است:
https://medium.com/media/74722e310c448c05fe052d33f884e46c/href
انعکاس در یک فایل ساخت چیزی نیست که من دوست داشته باشم ببینم. به این دلیل است که جایگزین مکان ثابتی برای دارایی های تولید شده نبود، و بخش بعدی به وجود یکی بستگی داشت.
شبکه ایمنی: Roborazzi در ماژول KMP
فایروال رگرسیون بصری BikeHub Roborazzi است که تحت Robolectric اجرا می شود. تستها در مجموعه منبع AndroidHostTest هر ماژول قرار دارند———نوع تست سمت میزبان کتابخانه هدف Android KMP———و یک افزونه معمولی، bikehub.multiplatform.screenshot، شامل آنها میشود. چهل ماژول آن را پیاده سازی می کند. تستها در کنار کدی که پوشش میدهند، در داخل خود ماژولهای کراس پلتفرم قرار دارند.
https://medium.com/media/4409920f71bc3726a85fd707dc2ad696/href
تست به همان سادگی است:
https://medium.com/media/6b3af255b337aa235b0f1e56ed274b05/href
کلاس پایه اشتراکی ScreenshotTest در هسته: تست بقیه موارد را انجام می دهد: هر مورد در روشن و تاریک، به مجموعه ای از دستگاه هایی که یک بار به عنوان مشخصات به جای نام دستگاه تعریف شده اند، ارائه می شود، در مقیاس نیمه برای پایین نگه داشتن PNG ها، در مقایسه با آستانه 0.1.
و اینجا مشکل منابع برگشت
Robolectric رابط کاربری JVM را رندر می کند و منابع را از دارایی های تست واحد می خواند. منابع Multiplatform Compose به خودی خود به آنجا نرسیدند، بنابراین اسکرین شات ها با تصاویر مفقود و رشته های حل نشده بیرون آمدند.
این وصله نیمه دوم این افزونه مرسوم است: یک کار همگام سازی که منابع تولید شده را از خروجی ساخت هسته:res — محل پایدار ایجاد شده توسط راه حل بازتابی بالا — در دارایی های تست میزبان مرتب شده در حوله دارایی Android کپی می کند:
https://medium.com/media/f6fe102e5625222cb4aa081c2289b544/href
دو راه حل زنجیره ای، یکی در core:res و دیگری در افزونه اسکرین شات، برای قابل مشاهده کردن منابع ماژول Compose Multiplatform برای تست میزبان اندروید. کد برنامه درگیر در این خط صفر است. دوباره ابزار.
چه چیزی ارزشش را دارد و چه چیزی ارزشش را ندارد
مایلم قبل از کپی کردن این تنظیمات دو مبادله را در نظر بگیرید.
فقط یک پلت فرم را پوشش می دهد. رندرهای بین پلتفرمی را با استفاده از Skia در iOS و وب بنویسید. یک دسته سبز از اسکرین شات ها به من می گوید که composable ها در اندروید پایدار هستند و هیچ چیز در دو هدف دیگر وجود ندارد. به هر حال، اگر انجام می دهید، چگونه این شکاف را پوشش می دهید؟ هر راه حلی استقبال می شود 🙂
55 مگابایت فایل PNG در فضای ذخیره سازی موجود است. این تفاوت قیمتی است که می توانید به آن نگاه کنید. ضبط در مقیاس نیمه باعث می شود که خیلی بدتر نباشد. CI verifyRoborazziAndroidHostTest را اجرا میکند و در صورت درخواست کشش، زمانی که تأیید با شکست مواجه میشود، یک commit را بازنویسی میکند و میفرستد، که حلقه را در حرکت نگه میدارد، اما به این معنی است که اگر به اندازه کافی با دقت نگاه نکنید، میتوان یک تغییر بصری ناخواسته را پذیرفت.
چی رو بردارم
قبل از تغییر انواع ماژول، ابزار خود را بررسی کنید. تعویض com.android.library با com.android.kotlin.multiplatform.library یک تغییر تک خطی طولانی است. هر چیزی که دارای کلید «این ماژول کتابخانه Android است» باشد———تست ها، پیش نمایش ها، بسته بندی منابع——پس از آن نیاز به یک برنامه دارد و برنامه ریزی برای آن ارزان تر از کشف آن است.
کد پلت فرم را به دلایل غیر مرتبط با KMP از لایه ارائه خارج کنید. این تمرین روی یک پلت فرم نیز خوب است. اتفاقاً تصمیم میگیرید که لایه UI شما باید در یک بعد از ظهر حرکت کند یا یک ماه.
بعدی
قسمت 4: ارسال WebAssembly به تولید. ساختن یک اپلیکیشن کراس پلتفرم یک چیز است. کارکرد قابل اعتماد آن در مرورگر یکی دیگر از موارد است: کش کردن، بسته بزرگ، پیوندهای عمیق و مدل امنیتی مرورگر.
انتقال یک برنامه اندروید واقعی به Kotlin Multiplatform 3/4 — به اشتراک گذاری UI بدون از دست دادن شبکه ایمنی در ابتدا در ProAndroidDev در Medium پست شده بود، جایی که افراد با برجسته کردن و پاسخ دادن به این داستان به گفتگو ادامه می دهند.




