ترکیبهای مؤثر نقشه: نشانگرهای قابل کشیدن

این پست به بررسی الگوهای مؤثر و بهترین روشها برای کتابخانه GitHub-نوشتن نقشههای اندروید ادامه میدهد. برای معرفی این مجموعه و ایجاد زمینه های مشترک به مقاله اول مراجعه کنید. نمونه کامل و قابل اجرا برای این پست در نسخه فعلی android-maps-compose در GitHub موجود است.
پس از پوشش نشانگرهای غیرقابل کشیدن در پست قبلی، این مقاله تمرکز را به نشانگرهای قابل کشیدن معطوف کرده است.
TL;DR: با یک نشانگر قابل کشیدن، از الگوی زیر برای مقداردهی اولیه موقعیت آن از مدل استفاده کنید و به آن اجازه دهید از رویدادهای کشیدن کاربر پس از آن بهروزرسانی شود. اگر این کافی نیست، هر زمان که موقعیت مدل نشانگر از منابع خارجی تغییر کرد، از کلید Composable برای جایگزینی Marker و MarkerState استفاده کنید:
@Composable
fun DraggableMarker(
initialPosition: LatLng,
onUpdate: (LatLng) -> Unit
) {
val state = remember { MarkerState(initialPosition) }
Marker(state, draggable = true)
LaunchedEffect(Unit) {
snapshotFlow { state.position }
.collect { position -> onUpdate(position) }
}
}
برای بحث کامل به ادامه مطلب بروید.
پست قبلی یک الگوی مناسب برای کپسوله کردن وضعیت (Marker) ایجاد کرد:
@Composable
fun SimpleMarker(position: LatLng) {
val state = rememberUpdatedMarkerState(position)
Marker(state = state)
}
@Composable
fun rememberUpdatedMarkerState(newPosition: LatLng): MarkerState =
remember { MarkerState(position = newPosition, draggable = false) }
.apply { position = newPosition }
این تنها به این دلیل امکان پذیر است که یک منبع حقیقت وجود دارد: مدل خود برنامه.
اما چرا در وهله اول نیاز به این الگو وجود دارد؟ زیرا MarkerState به اندازه کافی عمومی است تا منابع رقیب حقیقت را در خود جای دهد: مدل داده برنامه و GoogleMap قدیمی (از Google Play Maps SDK) که میخواهد مالک این وضعیت باشد (برای منعکس کردن کاربر دستگاه که نشانگر را میکشد). با SimpleMarker
، هنگامی که نشانگر قابل کشیدن نیست، تماس گیرنده نیازی به برخورد با MarkerState نامناسب ندارد.
از نظر فنی، SimpleMarker صرفاً بهروزرسانیهای MarkerState را از داخل نادیده میگیرد Marker()
قابل ترکیب شیء حالت هنوز در داخل وجود دارد و میتوان آن را برای تغییرات مشاهده کرد، و GoogleMap قدیمی همچنان دارای وضعیت نمایش واقعی نشانگر است. با این حال، بدون کشیدن، هیچ تغییری در این حالت به غیر از مواردی که از طریق SimpleMarker کنترل میشوند، وجود ندارد: GoogleMap قدیمی هیچ راه سادهای برای مشاهده تغییرات وضعیت نشانگر در صورت عدم کشیدن ارائه نمیدهد، بنابراین حتی اگر وضعیت نمایش نشانگر قدیمی تغییر کند، هیچکس نمیداند. رویکرد SimpleMarker یک ساده سازی معماری ایمن است.
به محض اینکه کشش وارد عمل شود، منابع رقیب حقیقت می توانند اوضاع را پیچیده کنند. یک رویکرد ایدهآل مبتنی بر Compose به نحوی GoogleMap قدیمی را به عنوان منبع حقیقت حذف میکند و نشانگر را واقعاً بدون حالت در جریان دادههای یک طرفه میسازد. این حالت نشانگر را کاملاً محصور میکند و بالا میبرد و وضعیت را از رویدادهای کشیدن کاربر از طریق تماسهای برگشتی بهروزرسانی میکند. متأسفانه، این کار بدون جایگزینی معماری GoogleMap میراث دار ذاتی، که مستقیماً وضعیت نمایش نشانگر قدیمی را از رویدادهای ورودی کاربر به روز می کند، امکان پذیر نیست.
بدون توانایی حذف منبع متضاد حقیقت، هدف به راهحلهایی برای موارد استفاده خاص تغییر میکند. یک مورد رایج این است که موقعیت نشانگر را از محل مدل اولیه مقداردهی می کند و به کاربر اجازه می دهد موقعیت را پس از آن از طریق کشیدن کنترل کند. در اینجا، منبع اولیه و کوتاه مدت حقیقت (برای مقداردهی اولیه نشانگر) مدل است، و GoogleMap میراث به منبع حقیقت پس از مقداردهی اولیه تبدیل می شود. در این سناریو یک انتقال کنترل کاملاً تعریف شده وجود دارد. این شبیه به نحوه استفاده از الگوی حالت hoisted در Compose UI است: مقداردهی اولیه از مدل، سپس بهروزرسانی از منبع رویداد ورودی.
این مورد استفاده پیچیده تر از سناریوی پست اول است، با چندین گزینه اجرایی بالقوه قابل دوام. تنها با یک نشانگر، یک رویکرد ساده این است که MarkerState را نزدیک به محل استفاده آن در Marker Composable محصور کنیم:
@Composable
fun DraggableMarker(initialPosition: LatLng) {
val state = remember { MarkerState(initialPosition) }
Marker(state, draggable = true)
}
بسیار ساده: یک نشانگر قابل کشیدن که موقعیت اولیه آن از مدل داده می آید. پس از شروع، مدل را نادیده بگیرید و GoogleMap را به تنها منبع حقیقت تبدیل کنید.
این رفتار یادآور memoryMarkerState از Android-maps-compose API است که در پست قبلی در مورد آن صحبت کردم. هر چند این یکسان نیست. memoryMarkerState از memorySaveable در زیر کاپوت استفاده می کند که منبع دیگری از حقیقت را معرفی می کند. memorySaveable موقعیت MarkerState را به عنوان منطق UI تلقی می کند و زمانی که موقعیت نشانگر با یک مدل همگام می شود مناسب نیست.
کپسوله کردن MarkerState به این روش دارای چندین مزیت است:
- دسترسی رایگان به حالت را محدود می کند و وضوح کد را افزایش می دهد: هنگام بالا بردن حالت Composable، سطوح سلسله مراتب فراخوانی میانی دسترسی نوشتن به حالت hoisted پیدا می کنند و منابع بالقوه بیشتری برای حقیقت ایجاد می کنند.
- دسترسی ایالت را تقسیم بندی می کند: MarkerState وظایفی فراتر از ردیابی موقعیت نشانگر دارد. اینها معمولاً نباید از همان مکانهایی که با موقعیت نشانگر سروکار دارند قابل دسترسی باشند.
- آینده نگر: کتابخانه android-maps-compose ممکن است در طول زمان مسئولیت های مختلف MarkerState از جمله موقعیت نشانگر را تغییر دهد. کپسوله کردن MarkerState می تواند تا حدی برنامه را از چنین تغییراتی محافظت کند.
- از خطاهای بهروزرسانی همزمان جلوگیری میکند: MarkerState به وضعیت عکس فوری متکی است. بهروزرسانیهای همزمان وضعیت عکس فوری ممکن است شکست بخورد و استثنائات را ایجاد کند. محدود کردن MarkerState به ترکیب، استفاده غیر همزمان را تضمین می کند.
- مدل های متوسط را در خود جای می دهد: MarkerState یا بخشهایی از آن را انتخاب کنید، میتوان بخشی از یک مدل میانی باشد که در طول یک نشانگر مداوم که تعامل کاربر را میکشد، قبل از تداوم تغییرات در مدل دادههای سطح بالاتر در پایان یک تعامل کشیدن، سایر تغییرات رابط کاربری را تغذیه میکند. به روز رسانی مدل داده های سطح بالاتر در طول کشیدن فعال همیشه مناسب نیست و ممکن است پرهزینه باشد.
به طور معمول، موقعیت کشیدن نشانگر در مدل برنامه، از طریق یک تماس پاسخ داده میشود:
@Composable
fun DraggableMarker(
initialPosition: LatLng,
onUpdate: (LatLng) -> Unit
) {
val state = remember { MarkerState(initialPosition) }
Marker(state, draggable = true)
LaunchedEffect(Unit) {
snapshotFlow { state.position }
.collect { position -> onUpdate(position) }
}
}
در اینجا رویدادهای به روز رسانی موقعیت نشانگر در زنجیره تماس جریان می یابد تا مدل داده را به روز کند. تغییرات مدل داده ممکن است در زنجیره به عقب برگردد، اما کد آنها را نادیده میگیرد، زیرا دولت قبلاً آنها را دارد، و آنها منبع متضادی حقیقت خواهند بود.
نتیجه یک API همه منظوره قابل استفاده مجدد دیگر است، DraggableMarker
، که وجود MarkerState را به عنوان یک جزئیات پیاده سازی محصور نگه می دارد. جفت شده با SimpleMarker
از پست قبلی، این ترکیبی از الگوهای سطح بالاتر Marker API را تشکیل می دهد که موارد استفاده اولیه را بدون افشای خارجی MarkerState پوشش می دهد.
یک پیاده سازی جایگزین می تواند MarkerState را به سطح مدل ارتقا دهد. این امر نیاز به عبور از اطراف را از بین می برد initialPosition
پارامتر؛ این فقط برای ترکیب اولیه استفاده می شود، نه ترکیب مجدد. در تئوری، بالا بردن قابلیت آزمایش را نیز بهبود میبخشد، زیرا موقعیت نشانگر میتواند از یک آزمایش برای شبیهسازی کشیدن بهروزرسانی شود. با این حال، سایر ویژگی های MarkerState مربوط به کشیدن را نمی توان به این روش دستکاری کرد. از سوی دیگر، هزینه، کپسوله سازی MarkerState کمتر موثر است و مزایای مختلفی را که در بالا ذکر شد از دست می دهد. بعید است عملکرد بهبود یابد، زیرا کامپایلر Compose باید بتواند اساساً بهینه سازی را حذف کند. initialPosition
پارامتر برای ترکیب مجدد
پیادهسازی جایگزین زمانی مفیدتر به نظر میرسد که با مجموعهای از نشانگرهای قابل کشیدن سر و کار داریم تا یک نشانگر واحد: دیگر یک پارامتر اولیه برای ارسال وجود ندارد، بلکه یک دسته کامل وجود دارد. در این سناریو، کپسوله کردن مجموعه ای از MarkerStates در یک شیء دارنده حالت می تواند از منابع بالقوه بیشتر حقیقت جلوگیری کند. در پست بعدی به مثالی خواهم پرداخت.
در موردی که بهروزرسانیهای خارجی مدل دادهها نیاز به بهروزرسانی موقعیت نشانگر به مکان جدید پس از راهاندازی دارد، احتمالاً در حالی که نشانگر در حال کشیدن است، چطور؟ اجرای صحیح این کار می تواند مشکل باشد، اما یک رویکرد کلی ساده و تمیز وجود دارد: Marker و MarkerState را به طور کامل جایگزین کنید، معمولاً با استفاده از کلید Composable. در پست های بعدی نمونه هایی را ارائه خواهم کرد. من در اینجا اضافه می کنم که از نظر فنی باید فقط جایگزین MarkerState بدون جایگزین کردن Marker Composable کافی باشد. Marker()
باید بدون تابعیت باشد، در حالی که MarkerState
تمام قسمت های حالت دار را در بر می گیرد. در حال حاضر کافی نیست، به طور کلی، صرفاً به دلیل اشکالات در پیاده سازی android-maps-compose.
اگر رویکردهای فوق به دلایلی ناکافی باشند، ممکن است اوضاع زشتتر شود. داشتن منابع متعدد حقیقت می تواند منجر به مسابقه داده یا منطق کد پیچیده شود. در اینجا برخی از مشکلات احتمالی وجود دارد:
-
خطاهای به روز رسانی همزمان: MarkerState حالت عکس فوری است. اعمال همزمان به روز رسانی ممکن است ناموفق باشد:
- اجرای Android-maps-compose فعلی در حال حاضر هنگام بهروزرسانی MarkerState با شکست مواجه نمیشود، اما تضمینی وجود ندارد که به همین شکل باقی بماند.
- برعکس، اعمال بهروزرسانیهای همزمان از کد برنامه ممکن است در هر زمان برای عکسهای فوری غیرجهانی با شکست مواجه شود. (اگر به صراحت از اسنپ شات استفاده نمی کنید، ممکن است زیاد نگران نباشید.)
- مصنوعات بصری: بهروزرسانیهای همزمان میتوانند باعث سوسو زدن یا پرش نشانگر روی نقشه شوند.
- مسابقه داده ها: بهروزرسانیهای همزمان، پیشبینی اینکه آیا عمل کشیدن کاربر یا یک بهروزرسانی برنامهریزی برنده خواهد شد یا خیر، تعیین میکند که نشانگر به کجا ختم میشود.
اجرای Android-maps-compose کنونی MarkerState را از رشته اصلی بهروزرسانی میکند و اگر کد برنامه نیز بهروزرسانیهای MarkerState را فقط از رشته اصلی انجام دهد، رقابت دادهها را کمتر نگران میکند.
گزینه های دیگر برای پرداختن به مشکل منبع دوگانه حقیقت ممکن است بسته به مورد استفاده قابل اجرا باشد.
من را از طریق پیوند نمایه من دنبال کنید تا در جریان پست های آینده در این مجموعه و سایر موضوعات توسعه باشید.
انجام دهید شما نظری در مورد این موضوع دارید؟ نظر خود را در زیر در نظر بگیرید. APIهای نقشههای قابل ترکیب هنوز در مراحل ابتدایی خود هستند و قلمروهای ناشناخته زیادی وجود دارد.
اگر در مورد پروژه Compose مربوط به نقشه های خود به کمک حرفه ای نیاز دارید، لطفاً از طریق نمایه من با من تماس بگیرید. من اطلاعات دقیقی در مورد سطح API Maps Compose، اجزای داخلی آن، و ایدههای زیادی در مورد چگونگی رفع نواقص آن دارم.

انتساب: تصویر روی جلد در بالای پست ایجاد شده با DALL-E