در یک سال گذشته، بخش اصلی اپلیکیشن Digital Carrot را با GoMobile توسعه دادهام. این تجربه در مجموع بسیار مثبت بود و در این مدت نکات زیادی درباره توسعه اپلیکیشنهای موبایل با زبان Go یاد گرفتم. در این مقاله قصد دارم تمام این تجربهها، مزایا، محدودیتها و چالشهایی که با آنها روبهرو شدم را با شما به اشتراک بگذارم.
چرا GoMobile را انتخاب کردم؟
اگر بخواهم کوتاه جواب بدهم، دلیلش این است که به زبان Go علاقه دارم و دوست داشتم از آن در یک پروژه واقعی استفاده کنم. اما دلایل فنی مهمتری هم پشت این انتخاب وجود داشت.
اول اینکه Digital Carrot یک اپلیکیشن کراس پلتفرم است و به راهکاری نیاز داشتم که بتوانم کدهای آن را به سادگی برای پلتفرمهای مختلف کامپایل و اجرا کنم.
دلیل دوم، اکوسیستم قدرتمند Go بود. این زبان کتابخانههای متنوعی برای توسعه قابلیتهای سیستمی در اختیارم قرار میدهد و همین موضوع باعث شد بتوانم برنامه را به شکلی طراحی کنم که توسعه و سفارشیسازی آن در آینده سادهتر باشد.
برای مثال، از کتابخانه Expr برای ساخت و اجرای عبارتها (Expression) و از Goja برای اضافه کردن امکان توسعه افزونهها با جاوااسکریپت استفاده کردم. این قابلیتها نقش مهمی در انعطافپذیرتر شدن معماری برنامه داشتند.
GoMobile در این معماری چگونه استفاده میشود؟
در این پروژه، GoMobile مسئول ساخت رابط کاربری نیست، بلکه نقش اصلی آن مدیریت منطق بیزینس (Business Logic) برنامه است. به عبارت دیگر، تمام پردازشهای اصلی، قوانین برنامه، مدیریت دادهها و قابلیتهایی که مستقل از رابط کاربری هستند، در لایه Go پیادهسازی شدهاند.
در ابتدا استفاده از Fyne به عنوان فریمورک رابط کاربری مورد بررسی قرار گرفت. Fyne یک فریمورک نوشتهشده با Go است که امکان ساخت رابطهای گرافیکی را فراهم میکند، اما در زمان توسعه این پروژه هنوز به بلوغ کافی برای استفاده در یک اپلیکیشن موبایل واقعی نرسیده بود. به همین دلیل، معماری نهایی به این شکل تغییر کرد که Flutter وظیفه ساخت رابط کاربری را بر عهده داشته باشد و Go به عنوان Backend داخلی اپلیکیشن عمل کند.
این معماری باعث میشود هر بخش از سیستم در حوزهای که عملکرد بهتری دارد استفاده شود:
- Flutter برای ساخت رابط کاربری، مدیریت صفحات و تعامل با کاربر
- Go برای اجرای منطق برنامه، پردازشها و قابلیتهای قابلانتقال بین پلتفرمها
- Swift و Kotlin برای دسترسی به APIهای اختصاصی سیستمعامل
ارتباط Flutter و Go با استفاده از Protobuf
در معماری GoMobile، ارتباط بین Flutter و Go بر پایه پیامهای Protocol Buffers (Protobuf) انجام میشود. استفاده از Protobuf در این بخش اهمیت زیادی دارد، زیرا باعث میشود بدون نیاز به نوشتن کدهای تکراری برای تبدیل دستی دادهها، بتوان در هر دو سمت ارتباط یعنی Dart و Go با ساختارهای دادهای مناسب کار کرد.
در روشهای معمول، برای ارسال داده بین زبانهای مختلف معمولا از JSON استفاده میشود. در این حالت توسعهدهنده باید فرآیندهای Serialization و Deserialization را برای تبدیل Objectها به JSON و برعکس مدیریت کند. اما در Protobuf، ساختار پیامها یک بار تعریف میشود و ابزارهای مربوطه به صورت خودکار کلاسها و Structهای موردنیاز را برای زبانهای مختلف تولید میکنند.
برای مثال، میتوان یک پیام را در فایل Protobuf تعریف کرد و سپس همان ساختار را در Dart و Go در اختیار داشت:
message Function1API {
message Request {}
message Response {}
Request request = 1;
Response response = 2;
}
یکی دیگر از دلایل مناسب بودن Protobuf در این معماری، محدودیت Platform Channelهای Flutter است. ارتباط بین Flutter، کدهای بومی (Swift/Kotlin) و Go تنها از طریق دادههای ساده مانند:
- String
- Binary Data
- Boolean
- اعداد
امکانپذیر است. پیامهای Protobuf در نهایت به دادههای باینری تبدیل میشوند، بنابراین کاملا با این مدل ارتباطی سازگار هستند.
استفاده از یک API Channel به جای چندین Channel
یکی از مشکلات Platform Channel در Flutter این است که برای هر قابلیت جدید باید تغییرات در چند بخش مختلف ایجاد شود:
- کد Flutter
- کد بومی iOS یا Android
- لایه Go
با افزایش تعداد APIها، این روش باعث افزایش حجم کدنویسی تکراری و دشوار شدن نگهداری پروژه میشود.
برای حل این مشکل، به جای ایجاد یک Channel جداگانه برای هر عملیات، یک پیام اصلی طراحی میشود که تمام درخواستها را مدیریت میکند.
برای مثال، یک پیام عمومی به نام CarrotAPI میتواند شامل چندین API مختلف باشد:
message CarrotAPI {
oneof api {
Function1API function1 = 10;
Function2API function2 = 11;
}
}
در این ساختار، بخش oneof مشخص میکند که کدام عملیات باید اجرا شود. هر قابلیت نیز پیام مخصوص خود را دارد:
message Function1API {
message Request {}
message Response {}
Request request = 1;
Response response = 2;
}
فرآیند اجرای یک درخواست به این شکل است:
- Flutter اطلاعات موردنیاز را در بخش
Requestقرار میدهد. - پیام داخل شیء
CarrotAPIقرار میگیرد. - پیام به Go ارسال میشود.
- Go با بررسی مقدار
CarrotAPI.apiتشخیص میدهد کدام تابع باید اجرا شود. - نتیجه عملیات در بخش
Responseقرار گرفته و به Flutter برمیگردد.
این روش شاید نسبت به ایجاد Channelهای جداگانه کمی پیچیدهتر باشد، اما باعث میشود حجم زیادی از کدنویسی تکراری در Swift و Kotlin حذف شود و یک مسیر ارتباطی استاندارد برای تمام APIها وجود داشته باشد.
ارتباط Go با Swift و Kotlin
ارتباط در جهت مخالف، یعنی ارسال درخواست از Go به کدهای بومی سیستمعامل، کمی متفاوت است. GoMobile برای این کار از Interfaceها استفاده میکند.
در این روش ابتدا یک Interface در Go تعریف میشود که متدهای موردنیاز برای تعامل با سیستمعامل را مشخص میکند.
برای مثال:
type IosMethods interface {
// Screentime
SetShields([]byte) bool
HasScreentimePermissions() bool
}
این Interface سپس توسط GoMobile به یک Interface قابل استفاده در Objective-C یا Kotlin تبدیل میشود:
@interface MobileIosMethods : NSObject <goSeqRefInterface, MobileIosMethods>
@property(strong, readonly) _Nonnull id _ref;
- (BOOL)hasScreentimePermissions;
- (BOOL)setShields:(NSData* _Nullable)p0;
@end
حالا میتوان این Interface تولیدشده را در Swift یا Kotlin پیادهسازی کرد.
برای مثال در iOS:
class GoScreentime: NSObject, MobileIosMethodsProtocol {
public func setShields(_ p0: Data?) -> Bool {
return setShieldsFromJSONBytes(p0)
}
public func hasScreentimePermissions() -> Bool {
return AuthorizationCenter.shared.authorizationStatus == .approved
}
}
در مرحله راهاندازی برنامه، نمونه این کلاس به Go داده میشود:
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions:
[UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
self.carrotApi = DigitalCarrot.MobileNewAppleMobileAPI(
GoScreentime()
)
return true
}
}
از این مرحله به بعد، هر زمان Go یکی از متدهای Interface را فراخوانی کند، اجرای آن به کد Swift یا Kotlin منتقل میشود.
این روش برای دسترسی به قابلیتهایی که فقط در سیستمعامل وجود دارند بسیار کاربردی است؛ مانند:
- مدیریت مجوزها
- دسترسی به اطلاعات سلامت کاربر
- Screen Time
- اعلانها
- سرویسهای سختافزاری
البته این Interfaceها مانند Platform Channelها تنها از نوعهای داده ساده پشتیبانی میکنند. استفاده از Protobuf در این بخش نیز امکانپذیر است، اما زمانی ارزش بیشتری دارد که تعداد و پیچیدگی ارتباطات بین Go و کدهای بومی زیاد باشد. در پروژههایی که تعداد این فراخوانیها محدود است، استفاده از همان نوعهای ساده معمولا انتخاب منطقیتری است.
این معماری در عمل چگونه عمل کرده است؟
استفاده از ترکیب GoMobile، فلاتر و گولنگ در توسعه Digital Carrot نشان داده است که این معماری میتواند یک راهکار قدرتمند برای ساخت اپلیکیشنهای چندسکویی باشد. جدا کردن رابط کاربری از منطق اصلی برنامه، علاوه بر ایجاد انعطافپذیری بیشتر، مزایای مهمی در توسعه، تست و نگهداری پروژه ایجاد میکند.
البته مانند هر معماری دیگری، این روش نیز نقاط قوت و محدودیتهای خاص خود را دارد. در ادامه مهمترین مزایای این رویکرد بررسی میشوند.
مزایای استفاده از GoMobile
یکپارچگی ساده با سرور Go
یکی از مزیتهای مهم این معماری، زمانی مشخص میشود که سرور و کلاینت با یک زبان مشترک توسعه داده شده باشند.
سرور همگامسازی (Sync Server) Digital Carrot نیز با Go نوشته شده است. به همین دلیل، تست کردن فرآیند همگامسازی بسیار سادهتر شده است، زیرا کدهای مربوط به کلاینت میتوانند مستقیما در تستهای سرور استفاده شوند.
در معماریهایی که کلاینت و سرور با تکنولوژیهای کاملا متفاوت ساخته شدهاند، معمولا برای تست نیاز به راهاندازی محیطهای پیچیده وجود دارد؛ مثلا اجرای همزمان چند سرویس با Docker یا ساخت محیطهای آزمایشی جداگانه.
اما در اینجا کافی است تستها با دستور معمول Go اجرا شوند:
go test
به دلیل اشتراک زبان و کتابخانهها، تستهای مربوط به همگامسازی میتوانند بدون نیاز به اجرای کامل اپلیکیشن موبایل انجام شوند و زمان اجرای آنها نیز بسیار کوتاه است.
مرزبندی واضح بین منطق برنامه و رابط کاربری
یکی از مهمترین مزایای این معماری، جداسازی کامل Business Logic از Presentation Layer است.
در این ساختار:
- Flutter مسئول نمایش رابط کاربری و تعامل با کاربر است.
- Go مسئول منطق اصلی برنامه مانند ارتباط با سرور، ذخیرهسازی دادهها، اعتبارسنجی و پردازشها است.
از آنجا که این دو بخش با زبانهای متفاوتی نوشته شدهاند، احتمال ترکیب شدن منطق برنامه با کدهای رابط کاربری بسیار کاهش پیدا میکند.
این جداسازی فرآیند تست را نیز سادهتر میکند. تمام منطق برنامه میتواند مستقل از رابط کاربری در Go تست شود:
go test
در نتیجه، برای بررسی عملکرد برنامه نیازی نیست دائما عناصر رابط کاربری ایجاد و شبیهسازی شوند.
تست API و Replay Testing
یکی از مزایای جالب این معماری، امکان انجام Replay Testing است. در این روش، رفتار کاربر در رابط کاربری ثبت میشود، یعنی درخواستهایی که UI به لایه Go ارسال میکند ذخیره میشوند. سپس همان درخواستها میتوانند بدون اجرای دوباره رابط کاربری، به صورت برنامهریزیشده اجرا شوند.
برای مثال:
- کاربر در اپلیکیشن یک عملیات انجام میدهد.
- درخواستهای API تولیدشده ثبت میشوند.
- همان درخواستها در تستهای Go دوباره اجرا میشوند.
- نتیجه با رفتار مورد انتظار مقایسه میشود.
این روش باعث میشود بخش زیادی از تستها بدون نیاز به تستهای سنگین UI انجام شود.
البته همچنان برخی تستهای رابط کاربری ضروری هستند، اما تعداد آنها میتواند بسیار کمتر باشد؛ زیرا منطق اصلی برنامه قبلا به صورت مستقل تست شده است.
امکان تغییر رابط کاربری در آینده
جدا کردن Business Logic از UI یک مزیت مهم دیگر دارد: وابستگی کمتر به فریمورک رابط کاربری.
اگر در آینده نیاز باشد تجربهای کاملا بومی برای هر پلتفرم ایجاد شود، میتوان رابط کاربری Flutter را با گزینههای دیگری جایگزین کرد:
- SwiftUI برای iOS
- Jetpack Compose یا Kotlin برای Android
در این حالت، بخش اصلی برنامه که در Go نوشته شده است بدون تغییر باقی میماند.
این موضوع اهمیت زیادی دارد، زیرا اکوسیستمهای رابط کاربری معمولا سریع تغییر میکنند و بعضی تکنولوژیها ممکن است در آینده کنار گذاشته شوند. اما منطق برنامه که در Go پیادهسازی شده است، وابستگی کمتری به این تغییرات دارد.
دسترسی به مجموعه گستردهای از کتابخانهها
این معماری امکان استفاده همزمان از اکوسیستم Go و Dart را فراهم میکند.
اکوسیستم Dart و Flutter ابزارهای مناسبی برای موارد مرتبط با رابط کاربری و قابلیتهای مخصوص موبایل دارد، مانند:
- مدیریت مجوزها
- دسترسی به قابلیتهای سیستمعامل
- تعامل با APIهای موبایل
در مقابل، Go مجموعه قدرتمندی از کتابخانهها برای مواردی مانند:
- شبکه
- پردازشهای سیستمی
- مدیریت همزمانی
- ارتباطات سرویسها
در اختیار توسعهدهنده قرار میدهد.
به این ترتیب، هر بخش از سیستم از ابزارهایی استفاده میکند که برای آن حوزه مناسبتر هستند.
اجرای منطق برنامه روی هر پروتکل ارتباطی
یکی از تصمیمهای مهم در این معماری، قرار دادن ارتباطات برنامه روی یک رابط مبتنی بر شبکه مانند Protocol است. این کار باعث میشود Backend برنامه محدود به یک محیط خاص نباشد و بتواند در مکانهای مختلف اجرا شود.
برای مثال، در نسخههای دسکتاپ Digital Carrot روی ویندوز و macOS، بخش Go به صورت یک Daemon در پسزمینه اجرا میشود و رابط کاربری از طریق ارتباطاتی مانند:
- gRPC
- Socket
- Pipe
به آن متصل میشود.
این معماری برای برنامهای مانند Digital Carrot اهمیت زیادی دارد، زیرا برنامه باید دائما در پسزمینه فعال باشد تا بتواند سایتها و برنامهها را مدیریت یا مسدود کند.
اگر کل رابط کاربری نیز همیشه در حافظه نگه داشته شود، مصرف منابع افزایش پیدا میکند. اما با اجرای مستقل Backend، مصرف حافظه برنامه بسیار کاهش پیدا کرده و تنها حدود ۳۰ تا ۶۰ مگابایت RAM برای اجرای پسزمینه کافی است.
این ایده تنها محدود به چنین اپلیکیشنهایی نیست. برای مثال، یک ویرایشگر متن میتواند Backend خود را روی یک سرور خانگی یا دستگاه محلی اجرا کند و رابطهای کاربری مختلف فقط به API آن متصل شوند.
آمادگی برای قابلیتهای مبتنی بر هوش مصنوعی
یکی دیگر از مزایای احتمالی این معماری، آماده بودن APIهای برنامه برای تعامل با ابزارهای خارجی مانند Agentهای هوش مصنوعی است.
وقتی تمام قابلیتهای برنامه از طریق APIهای مشخص در دسترس باشند، اضافه کردن قابلیتهای هوشمند سادهتر میشود.
به جای اینکه یک Agent هوش مصنوعی مجبور باشد مانند یک کاربر روی رابط کاربری کلیک کند و با عناصر بصری تعامل داشته باشد، میتواند مستقیما با APIهای موجود ارتباط برقرار کند.
این موضوع میتواند مسیر توسعه قابلیتهایی مانند:
- دستیار هوشمند داخل برنامه
- اجرای خودکار وظایف
- تحلیل دادههای برنامه
را در آینده سادهتر کند.
معایب و چالشهای استفاده از GoMobile
با وجود مزایای قابل توجه معماری GoMobile، این رویکرد مانند هر انتخاب معماری دیگری محدودیتهایی نیز دارد. برخی از این مشکلات مربوط به هزینههای ارتباط بین لایهها، پیچیدگی توسعه و برخی محدودیتهای خود GoMobile هستند.
عملکرد (Performance)
یکی از هزینههای اصلی این معماری، فرآیند تبدیل دادهها بین Flutter و Go است. هر بار که یک تابع بین این دو لایه فراخوانی میشود، پیام Protobuf باید ابتدا Serialize شود و سپس در سمت مقصد دوباره Deserialize شود.
این فرآیند اگرچه بسیار سریع است، اما بدون هزینه نیست و معمولا چند میلیثانیه تاخیر ایجاد میکند. برای اپلیکیشنی مانند Digital Carrot که بیشتر شامل عملیات مدیریتی، پردازش داده و تعامل با سرور است، این مقدار سربار کاملا قابل قبول است. اما برای برنامههایی که به عملکرد لحظهای نیاز دارند، مانند بازیهای ویدیویی یا نرمافزارهای گرافیکی سنگین، چنین تاخیری میتواند مشکلساز باشد.
در چنین برنامههایی معمولا نیاز است ارتباط بین بخشهای مختلف با کمترین سربار ممکن انجام شود و استفاده مداوم از Serialization و Deserialization میتواند به یک گلوگاه تبدیل شود.
پیچیدگی توسعه (Complexity)
اضافه کردن قابلیتهای جدید در این معماری نسبت به یک اپلیکیشن کاملا نوشتهشده با Flutter ساده نیست.
هر قابلیت جدید معمولا به چند مرحله نیاز دارد:
- تعریف پیامهای جدید در Protobuf
- تولید کدهای مربوط به Dart و Go
- پیادهسازی منطق موردنظر در Go
- اتصال API جدید به مسیر ارتباطی موجود
برای مثال، اضافه کردن یک قابلیت جدید فقط به نوشتن یک کلاس یا تابع جدید محدود نمیشود، بلکه باید قرارداد ارتباطی بین دو بخش نیز تعریف شود.
البته این پیچیدگی یک مزیت جانبی هم دارد؛ زیرا توسعهدهنده مجبور میشود قبل از اضافه کردن قابلیتهای جدید، معماری و API آن را دقیقتر طراحی کند و از ایجاد منطقهای پراکنده جلوگیری شود.
با این حال، استفاده از این معماری نیازمند آشنایی با چند فناوری مختلف است:
- Dart و Flutter
- Go
- Protobuf
- ابزارهای تولید کد برای هر زبان
بنابراین تیم توسعه باید توانایی کار با چند اکوسیستم مختلف را داشته باشد.
حجم بیشتر اپلیکیشن (Larger App Size)
یکی از مشکلات شناختهشده Go، حجم بالای فایلهای اجرایی آن است. در این معماری، Backend نوشتهشده با Go به همراه خود اپلیکیشن موبایل بستهبندی میشود و باعث افزایش حجم نهایی برنامه خواهد شد.
در Digital Carrot، فایل Backend حدود ۵۳ مگابایت حجم دارد. البته مقایسه دقیق با نسخهای که کاملا با Dart نوشته شده دشوار است، اما احتمالا حذف Go تنها مقدار محدودی از حجم نهایی برنامه کم میکند و تاثیر آن در تجربه کاربر چندان محسوس نخواهد بود.
ارتباط دوطرفه بین Go و Flutter
یکی از چالشهای مهم، ارسال پیام از Go به Flutter است.
مسیر معمول ارتباط به شکل زیر طراحی شده است:
Flutter → Go
اما در برخی شرایط، Go باید بتواند تغییرات را به Flutter اطلاع دهد. برای مثال:
- دریافت داده جدید از Sync
- تغییر وضعیت داخلی برنامه
- ارسال اعلان به رابط کاربری
Flutter ابزارهایی برای این نوع ارتباط دارد، اما ایجاد یک ارتباط پایدار و قابل اعتماد همیشه ساده نیست.
در نهایت، برای حل این مشکل یک راهکار ساده استفاده شده است: Flutter هر یک ثانیه وضعیت Backend را بررسی میکند و تغییرات را از Go دریافت میکند.
این روش از نظر معماری ایدهآل نیست، زیرا نوعی Polling محسوب میشود و میتواند منابع بیشتری مصرف کند، اما در مقابل بسیار ساده و قابل پیادهسازی است.
مشکلات مربوط به Async و مسدود شدن رابط کاربری
یکی از مشکلاتی که دیرتر مشخص شد، نحوه اجرای فراخوانیهای Go از Flutter بود.
در Flutter، فراخوانیهای Platform Channel به صورت پیشفرض همزمان (Synchronous) اجرا میشوند. یعنی زمانی که Flutter یک درخواست به Go ارسال میکند، Thread اصلی رابط کاربری منتظر دریافت پاسخ میماند.
اگر عملیات Go سریع باشد، مشکلی ایجاد نمیشود. اما اگر Backend نیاز به انجام عملیات سنگین داشته باشد، مثلا:
- دریافت اطلاعات از اینترنت
- پردازش حجم زیادی از دادهها
- اجرای عملیات زمانبر
رابط کاربری برای مدت کوتاهی متوقف میشود و کاربر کاهش روانی UI را احساس میکند.
راهحل این مشکل، اجرای عملیات در یک Goroutine جداگانه در Go است:
Flutter
|
| Request
↓
Go Backend
|
| Goroutine
↓
Long-running Task
|
| Callback
↓
Flutter UI Update
در این مدل، درخواست بلافاصله آزاد میشود و پس از پایان پردازش، نتیجه از طریق Callback به Flutter بازگردانده میشود.
این راهکار پیچیدگی بیشتری به کد اضافه میکند، اما برای جلوگیری از مسدود شدن رابط کاربری ضروری است.
مشکلات Time Zone و Networking
برخی مشکلات نیز مربوط به خود GoMobile هستند. یکی از مشکلات شناختهشده این است که در برخی نسخههای GoMobile، زمان محلی دستگاه به درستی تشخیص داده نمیشود و مقدار آن همیشه به GMT تنظیم میشود.
این موضوع میتواند باعث مشکلاتی در قابلیتهایی شود که به زمان محلی وابسته هستند، مانند:
- نمایش تاریخ و ساعت
- زمانبندیها
- رویدادهای وابسته به منطقه زمانی
راهحل موجود این است که اطلاعات Time Zone از Swift یا Kotlin دریافت شده و به Go ارسال شود. اگرچه این روش کار میکند، اما از نظر طراحی ایدهآل نیست، زیرا لایه اصلی برنامه باید به جای سیستمعامل، وابستگی مستقیم به اطلاعات منطقه زمانی داشته باشد.
مشکل دیگر مربوط به DNS Resolution در GoMobile روی iOS است.
در حالت شبیهساز، درخواستهای DNS معمولا بدون مشکل کار میکنند، اما هنگام اجرای برنامه روی دستگاه واقعی، ممکن است خطاهای مختلفی رخ دهد.
برای حل این مشکل باید کتابخانه libresolv به پروژه Xcode اضافه شود:
Xcode
|
└── libresolv
|
└── Go DNS Resolution
این مورد نمونهای از چالشهایی است که هنگام استفاده از ترکیب چند تکنولوژی مختلف ممکن است ظاهر شود، مشکلاتی که در یک اپلیکیشن کاملا نوشتهشده با Flutter یا Swift/Kotlin احتمالا وجود ندارند.
جمعبندی
در مجموع، استفاده از GoMobile برای توسعه Digital Carrot تجربه موفقی بوده است. با وجود اینکه این مسیر ساده نبوده و GoMobile هنوز در مقایسه با فریمورکهای رایج موبایل مانند فلاتر، React Native یا ابزارهای بومی iOS و Android استفاده گستردهای ندارد، اما نتیجه نهایی نشان میدهد که این معماری میتواند در سناریوهای مناسب بسیار قدرتمند باشد.
بخش زیادی از چالشها به دلیل کمبود مستندات و تجربههای عمومی درباره استفاده از GoMobile ایجاد میشوند. در بسیاری از موارد، راهکارها باید با آزمون و خطا پیدا شوند. با این حال، پس از عبور از این مشکلات، ترکیب Go و Flutter میتواند یک معماری پایدار و قابل اتکا ایجاد کند.
در حال دریافت نظرات از سرور، لطفا منتظر بمانید
در حال دریافت نظرات از سرور، لطفا منتظر بمانید