1. قبل البدء
يُعلّمك هذا الدرس التطبيقي حول كيفية دمج حزمة تطوير البرامج (SDK) لخدمة "خرائط Google" لنظام التشغيل Android مع تطبيقك واستخدام ميزاته الأساسية من خلال إنشاء تطبيق يعرض خريطة لمتاجر الدراجات في مدينة "سان فرانسيسكو" في ولاية "كاليفورنيا" بالولايات المتحدة الأمريكية.
المتطلّبات الأساسية
- معرفة أساسية بالتطوير بلغة Kotlin وAndroid
الإجراءات التي ستنفذّها
- يجب تفعيل حزمة تطوير البرامج (SDK) لخدمة "خرائط Google" لنظام التشغيل Android لإضافة "خرائط Google" إلى تطبيق متوافق مع Android.
- يمكنك إضافة علامات وتخصيصها وجمعها.
- ارسم خطوطًا ومضلّعات على الخريطة.
- التحكم في نقطة عرض الكاميرا آليًا
الأشياء التي تحتاج إليها
- SDK لـ "خرائط Google" لنظام التشغيل Android
- حساب Google تم تفعيل الفوترة به
- Android Studio 2020.3.1 أو إصدار أحدث
- خدمات Google Play المثبّتة في "استوديو Android"
- أن يكون لديك جهاز يعمل بنظام التشغيل Android أو محاكي Android يعمل على النظام الأساسي لواجهات Google APIs استنادًا إلى الإصدار 4.2.2 من نظام التشغيل Android أو الإصدارات الأحدث (راجِع تشغيل التطبيقات على محاكي Android لمعرفة خطوات التثبيت)
2. الإعداد
بالنسبة إلى خطوة التفعيل التالية، عليك تفعيل Maps SDK لنظام التشغيل Android.
إعداد "منصة خرائط Google"
إذا لم يكن لديك حساب على Google Cloud Platform ومشروع تم تفعيل الفوترة فيه، يُرجى الاطّلاع على دليل بدء استخدام "منصة خرائط Google" لإنشاء حساب فوترة ومشروع.
- في Cloud Console، انقر على القائمة المنسدلة للمشروع واختَر المشروع الذي تريد استخدامه لهذا الدرس التطبيقي.
- فعِّل واجهات برمجة تطبيقات ومنصة SDK لمنصة "خرائط Google" المطلوبة لهذا الدرس التطبيقي في Google Cloud Marketplace. ولإجراء ذلك، اتّبِع الخطوات الواردة في هذا الفيديو أو هذه المستندات.
- يمكنك إنشاء مفتاح واجهة برمجة تطبيقات في صفحة بيانات الاعتماد في Cloud Console. يمكنك اتّباع الخطوات الواردة في هذا الفيديو أو هذه المستندات. تتطلب جميع الطلبات إلى "منصة خرائط Google" مفتاح واجهة برمجة تطبيقات.
3- البدء بسرعة
لمساعدتك في البدء في أسرع وقت ممكن، إليك بعض رموز البدء لمساعدتك في متابعة هذا الدرس التطبيقي حول الترميز. يمكنك بدء استخدام الحل والانتقال إلى الحل، ولكن إذا كنت تريد متابعة كل الخطوات لتطويره بنفسك، يُرجى مواصلة القراءة.
- إنشاء نسخة طبق الأصل من المستودع في حال تثبيت
git
.
git clone https://github.com/googlecodelabs/maps-platform-101-android.git
ويمكنك بدلاً من ذلك النقر على الزر التالي لتنزيل رمز المصدر.
- بعد الحصول على الرمز، يمكنك فتح المشروع الذي يمكن العثور عليه داخل دليل
starter
في"استوديو Android".
4. إضافة "خرائط Google"
في هذا القسم، ستضيف "خرائط Google" حتى يتم تحميلها عند تشغيل التطبيق.
إضافة مفتاح واجهة برمجة التطبيقات
يجب تقديم مفتاح واجهة برمجة التطبيقات الذي أنشأته في خطوة سابقة للتطبيق حتى تتمكن حزمة تطوير البرامج (SDK) لخدمة "خرائط Google" لأجهزة Android من ربط مفتاحك بتطبيقك.
- ولإجراء ذلك، افتح الملف باسم
local.properties
في الدليل الجذري لمشروعك (المستوى نفسه حيثgradle.properties
وsettings.gradle
). - في هذا الملف، حدِّد مفتاحًا جديدًا
GOOGLE_MAPS_API_KEY
تكون قيمته هي واجهة برمجة التطبيقات التي أنشأتها.
local.properties
GOOGLE_MAPS_API_KEY=YOUR_KEY_HERE
لاحِظ أن local.properties
مدرَج في ملف .gitignore
في مستودع Git. ويرجع ذلك إلى أن مفتاح واجهة برمجة التطبيقات يُعتبر معلومات حساسة ويجب عدم تسجيله للوصول إلى عنصر التحكم في المصدر، إن أمكن.
- بعد ذلك، لعرض واجهة برمجة التطبيقات بحيث يمكن استخدامها في تطبيقك، أضِف المكوّن الإضافي Secrets Gradle Plugin for Android في ملف
build.gradle
الخاص بتطبيقك في الدليلapp/
وأضِف السطر التالي ضمن كتلةplugins
:
build.gradle على مستوى التطبيق
plugins {
// ...
id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
}
ستحتاج أيضًا إلى تعديل ملف build.gradle
على مستوى المشروع ليتضمن المسار التالي:
build.gradle على مستوى المشروع
buildscript {
dependencies {
// ...
classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:1.3.0"
}
}
سيتيح هذا المكوّن الإضافي أن تكون المفاتيح التي حدّدتها في ملف local.properties
متاحة كمتغيّرات إنشاء في ملف بيان Android وكمتغيرات في فئة BuildConfig
التي أنشأها Gradle في وقت الإصدار. ويؤدي استخدام هذا المكوّن الإضافي إلى إزالة الرمز النموذجي الذي قد يحتاج إلى قراءة خصائص من local.properties
حتى يمكن الوصول إليه في تطبيقك.
إضافة تبعية "خرائط Google"
- الآن ويمكن الوصول إلى مفتاح واجهة برمجة التطبيقات داخل التطبيق، وتتمثل الخطوة التالية في إضافة اعتماد حزمة تطوير البرامج (SDK) لتطبيق "خرائط Google" لنظام التشغيل Android إلى ملف
build.gradle
لتطبيقك.
في مشروع المبتدئين الذي يتضمن هذا الدرس التطبيقي حول الترميز، تمت إضافة هذه الاعتمادية لك من قبل.
build.gradle
dependencies {
// Dependency to include Maps SDK for Android
implementation 'com.google.android.gms:play-services-maps:17.0.0'
}
- بعد ذلك، أضِف علامة
meta-data
جديدة في العلامةAndroidManifest.xml
لتمرير مفتاح واجهة برمجة التطبيقات الذي أنشأته في خطوة سابقة. ولإجراء ذلك، يمكنك فتح هذا الملف في "استوديو Android" وإضافة العلامةmeta-data
التالية داخل العنصرapplication
في الملفAndroidManifest.xml
المتوفّر فيapp/src/main
.
ملف AndroidManifest.xml
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="${GOOGLE_MAPS_API_KEY}" />
- بعد ذلك، عليك إنشاء ملف تنسيق جديد باسم
activity_main.xml
في الدليلapp/src/main/res/layout/
وتحديده على النحو التالي:
activity_main.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<fragment
class="com.google.android.gms.maps.SupportMapFragment"
android:id="@+id/map_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
يحتوي هذا التنسيق على FrameLayout
واحد يحتوي على SupportMapFragment
. يحتوي هذا الجزء على عنصر GoogleMaps
الأساسي الذي تستخدمه في الخطوات اللاحقة.
- أخيرًا، عدِّل الفئة
MainActivity
فيapp/src/main/java/com/google/codelabs/buildyourfirstmap
من خلال إضافة الرمز التالي لإلغاء طريقةonCreate
بحيث يمكنك ضبط محتوياتها باستخدام التنسيق الجديد الذي أنشأته للتو.
النشاط الرئيسي
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
- والآن، شغِّل التطبيق وشغِّل خريطة الخريطة على شاشة جهازك.
5. تصميم الخرائط المستنِد إلى السحابة الإلكترونية (اختياري)
يمكنك تخصيص نمط الخريطة باستخدام نمط الخريطة المستنِدة إلى السحابة الإلكترونية.
إنشاء رقم تعريف للخريطة
إذا لم تكن قد أنشأت رقم تعريف خريطة باستخدام نمط خريطة مرتبط به، يمكنك مراجعة دليل أرقام تعريف الخريطة لإكمال الخطوات التالية:
- إنشاء رقم تعريف للخريطة.
- ربط رقم تعريف خريطة بنمط الخريطة.
إضافة معرّف الخريطة إلى تطبيقك
لاستخدام رقم تعريف الخريطة الذي أنشأته، عدِّل ملف activity_main.xml
ومرِّر رقم تعريف الخريطة في السمة map:mapId
من SupportMapFragment
.
activity_main.xml
<fragment xmlns:map="http://schemas.android.com/apk/res-auto"
class="com.google.android.gms.maps.SupportMapFragment"
<!-- ... -->
map:mapId="YOUR_MAP_ID" />
بعد إكمال ذلك، تابِع واعرض التطبيق لرؤية خريطتك بالنمط الذي اخترته.
6- إضافة علامات
في هذه المهمة، يمكنك إضافة علامات إلى الخريطة تمثّل نقاط الاهتمام التي تريد تمييزها على الخريطة. أولاً، يمكنك استرداد قائمة بالأماكن التي تم توفيرها لك في مشروع المبتدئين، ثم إضافة هذه الأماكن إلى الخريطة. في هذا المثال، هذه هي متاجر الدراجات.
الحصول على مرجع إلى "خرائط Google"
عليك أولاً الحصول على مرجع إلى عنصر GoogleMap
حتى تتمكن من استخدام أساليبه. ولإجراء ذلك، يمكنك إضافة الرمز التالي باستخدام طريقة MainActivity.onCreate()
بعد المكالمة مباشرةً إلى setContentView()
:
MainActivity.onCreate()
val mapFragment = supportFragmentManager.findFragmentById(
R.id.map_fragment
) as? SupportMapFragment
mapFragment?.getMapAsync { googleMap ->
addMarkers(googleMap)
}
تعثر عملية التنفيذ أولاً على SupportMapFragment
التي أضفتها في الخطوة السابقة باستخدام طريقة findFragmentById()
على الكائن SupportFragmentManager
. بعد الحصول على مرجع، يتم استدعاء طلب getMapAsync()
متبوعًا بتمرير lambda. لامدا هو العنصر الذي يتم فيه تمرير الكائن GoogleMap
. داخل lambda، تم استدعاء طريقة addMarkers()
، ويتم تحديدها قريبًا.
الصف المقدّم: PlacesReader
في مشروع المبتدئين، تم توفير الصف PlacesReader
لك. يقرأ هذا الصف قائمة من 49 مكانًا تم تخزينها في ملف JSON يُسمى places.json
وتعرض هذه الأماكن كـ List<Place>
. تمثل الأماكن نفسها قائمة بمتاجر الدراجات حول مدينة نصر، القاهرة، الولايات المتحدة الأمريكية.
إذا كنت مهتمًا بالاطّلاع على طريقة تنفيذ هذا الصف، يمكنك الوصول إليه على GitHub أو فتح الصف PlacesReader
في"استوديو Android".
أماكن القارئ
package com.google.codelabs.buildyourfirstmap.place
import android.content.Context
import com.google.codelabs.buildyourfirstmap.R
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import java.io.InputStream
import java.io.InputStreamReader
/**
* Reads a list of place JSON objects from the file places.json
*/
class PlacesReader(private val context: Context) {
// GSON object responsible for converting from JSON to a Place object
private val gson = Gson()
// InputStream representing places.json
private val inputStream: InputStream
get() = context.resources.openRawResource(R.raw.places)
/**
* Reads the list of place JSON objects in the file places.json
* and returns a list of Place objects
*/
fun read(): List<Place> {
val itemType = object : TypeToken<List<PlaceResponse>>() {}.type
val reader = InputStreamReader(inputStream)
return gson.fromJson<List<PlaceResponse>>(reader, itemType).map {
it.toPlace()
}
}
تحميل الأماكن
لتحميل قائمة متاجر الدراجات، أضف موقعًا في MainActivity
باسم places
وحدّده على النحو التالي:
الأماكن الرئيسية.
private val places: List<Place> by lazy {
PlacesReader(this).read()
}
يستدعي هذا الرمز الطريقة read()
في PlacesReader
، ما يعرض List<Place>
. تشتمل السمة Place
على خاصية تُسمى name
واسم المكان وlatLng
، وهي إحداثيات المكان.
المكان
data class Place(
val name: String,
val latLng: LatLng,
val address: LatLng,
val rating: Float
)
إضافة علامات إلى الخريطة
الآن وقد تم تحميل قائمة الأماكن إلى الذاكرة، فإن الخطوة التالية هي تمثيل هذه الأماكن على الخريطة.
- إنشاء طريقة في
MainActivity
باسمaddMarkers()
وتحديدها على النحو التالي:
MainActivity.addMarkers()
/**
* Adds marker representations of the places list on the provided GoogleMap object
*/
private fun addMarkers(googleMap: GoogleMap) {
places.forEach { place ->
val marker = googleMap.addMarker(
MarkerOptions()
.title(place.name)
.position(place.latLng)
)
}
}
يتم تكرار هذه الطريقة في قائمة places
متبوعة باستدعاء الطريقة addMarker()
في الكائن GoogleMap
المُقدّم. يتم إنشاء العلامة من خلال إنشاء مثيل لكائن MarkerOptions
، مما يسمح لك بتخصيص العلامة نفسها. في هذه الحالة، يتم توفير عنوان وموضع العلامة، والذي يمثل اسم متجر الدراجات وإحداثياتها، على التوالي.
- واصِل تشغيل التطبيق وتوجِّهه إلى سان فرانسيسكو للاطّلاع على العلامات التي أضفتها للتو.
7- تخصيص العلامات
هناك العديد من خيارات التخصيص للعلامات التي أضفتها للتو لمساعدتهم على التميُّز وتقديم معلومات مفيدة للمستخدمين. في هذه المهمة، ستستكشف بعض هذه القيم من خلال تخصيص صورة كل علامة بالإضافة إلى نافذة المعلومات التي يتم عرضها عند النقر على علامة.
إضافة نافذة معلومات
تعرض نافذة المعلومات بشكل تلقائي عندما تنقر على علامة عنوانها ومقتطفها (في حال ضبطها). ويمكنك تخصيص ذلك ليتمكّن من عرض معلومات إضافية، مثل عنوان المكان وتقييمه.
إنشاء tag_info_contents.xml
أولاً، أنشِئ ملف تنسيق جديدًا باسم marker_info_contents.xml
.
- ولإجراء ذلك، انقر بزر الماوس الأيمن على المجلد
app/src/main/res/layout
في طريقة عرض المشروع في "استوديو Android"، ثم اختَر جديد > ملف موارد التنسيق.
- في مربع الحوار، اكتب
marker_info_contents
في الحقل اسم الملف وLinearLayout
في الحقلRoot element
، ثم انقر على حسنًا.
ويتم تضخيم ملف التنسيق هذا لاحقًا لتمثيل المحتوى في نافذة المعلومات.
- انسخ المحتوى في مقتطف الرمز التالي، الذي يضيف ثلاثة
TextViews
ضمن مجموعة عرضLinearLayout
عمودية، وتحلّل الرمز التلقائي في الملف.
mark_info_contents.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:padding="8dp">
<TextView
android:id="@+id/text_view_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/black"
android:textSize="18sp"
android:textStyle="bold"
tools:text="Title"/>
<TextView
android:id="@+id/text_view_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/black"
android:textSize="16sp"
tools:text="123 Main Street"/>
<TextView
android:id="@+id/text_view_rating"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/black"
android:textSize="16sp"
tools:text="Rating: 3"/>
</LinearLayout>
إنشاء تنفيذ InfoWindow يتوفر محوّل
بعد إنشاء ملف التنسيق لنافذة المعلومات المخصصة، تتمثل الخطوة التالية في تنفيذ واجهة GoogleMap.InfoWindowAdaptiveer. وتتضمّن هذه الواجهة طريقتَين، getInfoWindow()
وgetInfoContents()
. تعرض كلتا الطريقتَين عنصر View
اختياري، حيث يتم استخدام العنصر السابق لتخصيص النافذة نفسها، والعكس صحيح، وهو تخصيص محتواها. وفي الحالة الخاصة بك، نفّذت كلاً من القيمتَين المخصّصتَين وعرض القيمة getInfoContents()
مع عرض القيمة فارغة في getInfoWindow()
، ما يشير إلى أنّه يجب استخدام النافذة التلقائية.
- أنشِئ ملف Kotlin جديدًا باسم
MarkerInfoWindowAdapter
في حزمةMainActivity
نفسها عن طريق النقر بزر الماوس الأيمن على المجلدapp/src/main/java/com/google/codelabs/buildyourfirstmap
في عرض المشروع على "استوديو Android"، ثم اختيار New > Kotlin File/Class.
- في مربع الحوار، اكتب
MarkerInfoWindowAdapter
واجعل الملف محددًا.
- بعد إنشاء الملف، انسخ المحتوى المتوفّر في مقتطف الرمز التالي والصقه في الملف الجديد.
MarkerInfoWindowAdaptiveer
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.model.Marker
import com.google.codelabs.buildyourfirstmap.place.Place
class MarkerInfoWindowAdapter(
private val context: Context
) : GoogleMap.InfoWindowAdapter {
override fun getInfoContents(marker: Marker?): View? {
// 1. Get tag
val place = marker?.tag as? Place ?: return null
// 2. Inflate view and set title, address, and rating
val view = LayoutInflater.from(context).inflate(
R.layout.marker_info_contents, null
)
view.findViewById<TextView>(
R.id.text_view_title
).text = place.name
view.findViewById<TextView>(
R.id.text_view_address
).text = place.address
view.findViewById<TextView>(
R.id.text_view_rating
).text = "Rating: %.2f".format(place.rating)
return view
}
override fun getInfoWindow(marker: Marker?): View? {
// Return null to indicate that the
// default window (white bubble) should be used
return null
}
}
في محتوى طريقة getInfoContents()
، يتم إرسال العلامة المحدّدة في الطريقة إلى النوع Place
، وإذا لم يكن من الممكن الإرسال، تعرض الطريقة قيمة فارغة (لم يتم ضبط سمة العلامة على Marker
بعد، ولكن سيتم ذلك في الخطوة التالية).
بعد ذلك، يتم تضخيم التنسيق marker_info_contents.xml
من خلال ضبط النص الذي يحتوي على TextViews
إلى العلامة Place
.
تعديل النشاط الرئيسي
للصق جميع المكوّنات التي أنشأتها حتى الآن، عليك إضافة سطرين في صف MainActivity
.
أولاً، لإدخال InfoWindowAdapter
المخصّصة، MarkerInfoWindowAdapter
، داخل استدعاء طريقة getMapAsync
، عليك استدعاء طريقة setInfoWindowAdapter()
على العنصر GoogleMap
وإنشاء مثيل جديد من MarkerInfoWindowAdapter
.
- يمكنك إجراء ذلك من خلال إضافة الرمز التالي بعد استدعاء طريقة
addMarkers()
داخلgetMapAsync()
lambda.
MainActivity.onCreate()
// Set custom info window adapter
googleMap.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))
وأخيرًا، يجب تعيين كل مكان باعتباره موقع العلامة على كل علامة تتم إضافتها إلى الخريطة.
- لإجراء ذلك، عدِّل الاستدعاء
places.forEach{}
في الدالةaddMarkers()
باستخدام ما يلي:
MainActivity.addMarkers()
places.forEach { place ->
val marker = googleMap.addMarker(
MarkerOptions()
.title(place.name)
.position(place.latLng)
.icon(bicycleIcon)
)
// Set place as the tag on the marker object so it can be referenced within
// MarkerInfoWindowAdapter
marker.tag = place
}
إضافة صورة محدّد موقع
يُعد تخصيص صورة العلامة إحدى الطرق المرحة لتوضيح نوع المكان الذي تمثله العلامة على خريطتك. بالنسبة إلى هذه الخطوة، يمكنك عرض الدراجات بدلاً من العلامات الحمراء التلقائية لتمثيل كل متجر على الخريطة. يتضمّن مشروع بدء الاستخدام رمز الدراجة ic_directions_bike_black_24dp.xml
في app/src/res/drawable
، والذي تستخدمه.
ضبط صورة نقطية مخصّصة على محدّد الموقع
باستخدام رمز الدراجة القابل للرسم المتاح تحت تصرفك، تتمثّل الخطوة التالية في ضبط هذا الرسم كرمز لكل علامة على الخريطة. يشمل MarkerOptions
طريقة icon
، التي تعتمد على BitmapDescriptor
التي تستخدمها لتنفيذ ذلك.
أولاً، يجب تحويل المتّجه القابل للرسم الذي أضفته للتو إلى BitmapDescriptor
. يحتوي ملف باسم BitMapHelper
مضمّن في مشروع إجراء التفعيل على وظيفة مساعد اسمها vectorToBitmap()
.
BitmapHelper
package com.google.codelabs.buildyourfirstmap
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.util.Log
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.DrawableCompat
import com.google.android.gms.maps.model.BitmapDescriptor
import com.google.android.gms.maps.model.BitmapDescriptorFactory
object BitmapHelper {
/**
* Demonstrates converting a [Drawable] to a [BitmapDescriptor],
* for use as a marker icon. Taken from ApiDemos on GitHub:
* https://github.com/googlemaps/android-samples/blob/main/ApiDemos/kotlin/app/src/main/java/com/example/kotlindemos/MarkerDemoActivity.kt
*/
fun vectorToBitmap(
context: Context,
@DrawableRes id: Int,
@ColorInt color: Int
): BitmapDescriptor {
val vectorDrawable = ResourcesCompat.getDrawable(context.resources, id, null)
if (vectorDrawable == null) {
Log.e("BitmapHelper", "Resource not found")
return BitmapDescriptorFactory.defaultMarker()
}
val bitmap = Bitmap.createBitmap(
vectorDrawable.intrinsicWidth,
vectorDrawable.intrinsicHeight,
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(bitmap)
vectorDrawable.setBounds(0, 0, canvas.width, canvas.height)
DrawableCompat.setTint(vectorDrawable, color)
vectorDrawable.draw(canvas)
return BitmapDescriptorFactory.fromBitmap(bitmap)
}
}
تأخذ هذه الطريقة رقم تعريف المورد Context
القابل للرسم، بالإضافة إلى عدد صحيح للون وتنشئ BitmapDescriptor
تمثيل له.
باستخدام الطريقة المساعدة، حدِّد خاصية جديدة تحمل الاسم bicycleIcon
وامنحها التعريف التالي: MainActivity.bicycleIcon
private val bicycleIcon: BitmapDescriptor by lazy {
val color = ContextCompat.getColor(this, R.color.colorPrimary)
BitmapHelper.vectorToBitmap(this, R.drawable.ic_directions_bike_black_24dp, color)
}
تستخدم هذه الخاصية اللون colorPrimary
المحدد مسبقًا في تطبيقك، وتستخدم هذا التعديل لتلوين رمز الدراجة وعرضه كـ BitmapDescriptor
.
- باستخدام هذه الخاصية، يمكنك استدعاء الطريقة
icon
منMarkerOptions
في طريقةaddMarkers()
لإكمال تخصيص الرمز. عند إجراء ذلك، يجب أن تظهر خاصية العلامة كما يلي:
MainActivity.addMarkers()
val marker = googleMap.addMarker(
MarkerOptions()
.title(place.name)
.position(place.latLng)
.icon(bicycleIcon)
)
- شغِّل التطبيق للاطلاع على العلامات المحدَّثة.
8- علامات المجموعة
تبعًا لمدى تكبير الخريطة، قد تلاحظ أن العلامات التي أضفتها تتداخل. من الصعب جدًا التفاعل مع العلامات المتداخلة وإنشاء الكثير من الضوضاء، ما يؤثر في سهولة استخدام تطبيقك.
لتحسين تجربة المستخدم في هذا الشأن، كلما كانت لديك مجموعة بيانات كبيرة تم تجميعها عن كثب، من أفضل الممارسات تنفيذ تجميع العلامات. باستخدام التجميع، أثناء تكبير الخريطة وتصغيرها، يتم تجميع العلامات القريبة عندما تكون على النحو التالي:
لتنفيذ هذا، تحتاج إلى مساعدة SDK للخرائط لمكتبة برامج خدمات Android.
"خرائط Google" لمكتبة Android Utility
تم إنشاء "حزمة تطوير البرامج" (SDK) لتطبيق "خرائط Google" لنظام التشغيل Android كوسيلة لتوسيع نطاق عمل حزمة تطوير البرامج (SDK) لخدمة "خرائط Google" لنظام التشغيل Android. يقدم هذا الإصدار ميزات متقدمة، مثل تجميع العلامات وخرائط التمثيل اللوني ودعم KML وGeoJson، وترميز الترميز المتعدد الخطوط ومجموعة من الوظائف المساعدة حول الهندسة الكروية.
تحديث ملف build.gradle
نظرًا لأن مكتبة الأدوات المساعدة مجمّعة بشكل منفصل عن"خرائط Google"SDK لنظام التشغيل Android، عليك إضافة تبعية إضافية إلى ملف build.gradle
.
- عدِّل القسم
dependencies
من ملفapp/build.gradle
.
build.gradle
implementation 'com.google.maps.android:android-maps-utils:1.1.0'
- عند إضافة هذا السطر، يجب إجراء مزامنة للمشروع لجلب تبعيات جديدة.
تنفيذ التجميع
لتنفيذ التجميع على تطبيقك، اتّبع الخطوات الثلاث التالية:
- تنفيذ واجهة
ClusterItem
. - الفئة الفرعية
DefaultClusterRenderer
. - يمكنك إنشاء
ClusterManager
وإضافة عناصر.
تنفيذ واجهة ClusterItem
تحتاج جميع العناصر التي تمثل علامة قابلة للتجميع على الخريطة إلى تنفيذ واجهة ClusterItem
. وفي هذه الحالة، يعني ذلك أن النموذج Place
يجب أن يتوافق مع ClusterItem
. يمكنك فتح ملف Place.kt
وإجراء التعديلات التالية عليه:
المكان
data class Place(
val name: String,
val latLng: LatLng,
val address: String,
val rating: Float
) : ClusterItem {
override fun getPosition(): LatLng =
latLng
override fun getTitle(): String =
name
override fun getSnippet(): String =
address
}
تحدد ClusterItem الطرق الثلاث التالية:
getPosition()
، الذي يمثلLatLng
.getTitle()
، الذي يمثل اسم المكانgetSnippet()
، الذي يمثل عنوان المكان.
الفئة الفرعية DefaultClusterRenderer
يستخدم الصف ClusterManager
المسؤول عن تنفيذ التجميع، صفًا ClusterRenderer
داخليًا لمعالجة إنشاء المجموعات أثناء العرض الشامل والتكبير/التصغير حول الخريطة. بشكل تلقائي، يكون مزوّدًا بالعارض التلقائي DefaultClusterRenderer
، الذي ينفّذ ClusterRenderer
. وفي حالات بسيطة، يكفي ذلك. ومع ذلك، نظرًا لحاجتك إلى تخصيص العلامات، فإنك بحاجة إلى توسيع هذه الفئة وإضافة التخصيصات إليها.
ابدأ بإنشاء ملف Kotlin PlaceRenderer.kt
في الحزمة com.google.codelabs.buildyourfirstmap.place
وحدِّده على النحو التالي:
PlaceRenderer
package com.google.codelabs.buildyourfirstmap.place
import android.content.Context
import androidx.core.content.ContextCompat
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.model.BitmapDescriptor
import com.google.android.gms.maps.model.Marker
import com.google.android.gms.maps.model.MarkerOptions
import com.google.codelabs.buildyourfirstmap.BitmapHelper
import com.google.codelabs.buildyourfirstmap.R
import com.google.maps.android.clustering.ClusterManager
import com.google.maps.android.clustering.view.DefaultClusterRenderer
/**
* A custom cluster renderer for Place objects.
*/
class PlaceRenderer(
private val context: Context,
map: GoogleMap,
clusterManager: ClusterManager<Place>
) : DefaultClusterRenderer<Place>(context, map, clusterManager) {
/**
* The icon to use for each cluster item
*/
private val bicycleIcon: BitmapDescriptor by lazy {
val color = ContextCompat.getColor(context,
R.color.colorPrimary
)
BitmapHelper.vectorToBitmap(
context,
R.drawable.ic_directions_bike_black_24dp,
color
)
}
/**
* Method called before the cluster item (the marker) is rendered.
* This is where marker options should be set.
*/
override fun onBeforeClusterItemRendered(
item: Place,
markerOptions: MarkerOptions
) {
markerOptions.title(item.name)
.position(item.latLng)
.icon(bicycleIcon)
}
/**
* Method called right after the cluster item (the marker) is rendered.
* This is where properties for the Marker object should be set.
*/
override fun onClusterItemRendered(clusterItem: Place, marker: Marker) {
marker.tag = clusterItem
}
}
تلغي هذه الفئة الوظيفتين التاليتين:
onBeforeClusterItemRendered()
، وهو ما يتم استدعاءه قبل عرض المجموعة على الخريطة. يمكنك هنا توفير التخصيصات من خلالMarkerOptions
، وفي هذه الحالة، يتم ضبط عنوان العلامة وموضعها ورمزها.onClusterItemRenderer()
، والذي يتم استدعاءه بعد عرض العلامة مباشرةً على الخريطة. وهذا هو المكان الذي يمكنك من خلاله الوصول إلى عنصرMarker
الذي تم إنشاؤه - في هذه الحالة، يتم ضبط خاصية العلامة المحدّد.
إنشاء ClusterManager وإضافة عناصر
أخيرًا، لبدء العمل في تجميع، عليك تعديل MainActivity
لإنشاء ClusterManager
وإنشاء تبعيات ضرورية له. يتعامل ClusterManager
مع إضافة العلامات (عناصر ClusterItem
) داخليًا، لذلك بدلاً من إضافة العلامات مباشرةً على الخريطة، يتم تفويض هذه المسؤولية إلى ClusterManager
. بالإضافة إلى ذلك، يستدعي ClusterManager
أيضًا setInfoWindowAdapter()
داخليًا، لذا يجب ضبط نافذة معلومات مخصّصة على الكائن ClusterManger
's MarkerManager.Collection
.
- للبدء، عدِّل محتوى لمدا في استدعاء
getMapAsync()
فيMainActivity.onCreate()
. أَرْجُو الْاسْتِمْرَارْ وِتَعْلِيقِ الْاتِّصَالْ بِـaddMarkers()
وِsetInfoWindowAdapter()
، وِبَدْلِهَا اسْتِدْعَاءْ دِلْوَقْتِي اسْمُهْaddClusteredMarkers()
، الْلِّي بَيْتِمّْ تَحْدِيدُهْ.
MainActivity.onCreate()
mapFragment?.getMapAsync { googleMap ->
//addMarkers(googleMap)
addClusteredMarkers(googleMap)
// Set custom info window adapter.
// googleMap.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))
}
- بعد ذلك، عليك تحديد
addClusteredMarkers()
فيMainActivity
.
MainActivity.addClusteredMarkers()
/**
* Adds markers to the map with clustering support.
*/
private fun addClusteredMarkers(googleMap: GoogleMap) {
// Create the ClusterManager class and set the custom renderer.
val clusterManager = ClusterManager<Place>(this, googleMap)
clusterManager.renderer =
PlaceRenderer(
this,
googleMap,
clusterManager
)
// Set custom info window adapter
clusterManager.markerCollection.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))
// Add the places to the ClusterManager.
clusterManager.addItems(places)
clusterManager.cluster()
// Set ClusterManager as the OnCameraIdleListener so that it
// can re-cluster when zooming in and out.
googleMap.setOnCameraIdleListener {
clusterManager.onCameraIdle()
}
}
تعمل هذه الطريقة على إنشاء ClusterManager
، وتمرير العارض المخصص PlacesRenderer
، وإضافة جميع الأماكن، واستدعاء الطريقة cluster()
. وبما أنّ ClusterManager
يستخدم الطريقة setInfoWindowAdapter()
على عنصر الخريطة، سيكون عليك ضبط نافذة المعلومات المخصّصة على العنصر ClusterManager.markerCollection
. وأخيرًا، إذا أردت أن يتغير التجميع أثناء تحريك المستخدم وتكبير/تصغيره على الخريطة، يتم توفير OnCameraIdleListener
إلى googleMap
، بحيث عندما تكون الكاميرا في وضع عدم النشاط، يتم استدعاء clusterManager.onCameraIdle()
.
- واصِل تشغيل التطبيق لرؤية المتاجر المُجمَّعة الجديدة.
9- الرسم على الخريطة
أثناء استكشاف إحدى الطرق للرسم على الخريطة (من خلال إضافة علامات)، تتيح حزمة تطوير البرامج (SDK) لخدمة "خرائط Google" لأجهزة Android العديد من الطرق الأخرى التي يمكنك رسمها لعرض معلومات مفيدة على الخريطة.
على سبيل المثال، إذا كنت تريد تمثيل المسارات والمناطق على الخريطة، يمكنك استخدام الخطوط المتعددة والمضلعات لعرضها على الخريطة. أو إذا كنت تريد إصلاح صورة على سطح الأرض، يمكنك استخدام تراكبات الأرض.
وستتعلّم في هذه المهمة كيفية رسم أشكال، بشكل خاص على شكل دائرة، حول محدّد عند النقر عليها.
إضافة أداة معالجة النقر
عادةً ما تتمثّل الطريقة التي يمكنك من خلالها إضافة أداة معالجة نقرة إلى علامة في تمرير الملف من خلال أداة معالجة نقرة على عنصر GoogleMap
مباشرةً من خلال setOnMarkerClickListener()
. ومع ذلك، نظرًا لأنك تستخدم التجميع، يجب تقديم أداة معالجة النقر إلى ClusterManager
بدلاً من ذلك.
- في طريقة
addClusteredMarkers()
فيMainActivity
، يمكنك إضافة السطر التالي مباشرةً بعد الاستدعاء إلىcluster()
.
MainActivity.addClusteredMarkers()
// Show polygon
clusterManager.setOnClusterItemClickListener { item ->
addCircle(googleMap, item)
return@setOnClusterItemClickListener false
}
تضيف هذه الطريقة أداة معالجة وتستدعي الطريقة addCircle()
التي تحدّدها بعد ذلك. وأخيرًا، يتم عرض false
من هذه الطريقة للإشارة إلى أن هذه الطريقة لم تستهلك هذا الحدث.
- بعد ذلك، يجب تحديد الخاصية
circle
والطريقةaddCircle()
فيMainActivity
.
MainActivity.addCircle()
private var circle: Circle? = null
/**
* Adds a [Circle] around the provided [item]
*/
private fun addCircle(googleMap: GoogleMap, item: Place) {
circle?.remove()
circle = googleMap.addCircle(
CircleOptions()
.center(item.latLng)
.radius(1000.0)
.fillColor(ContextCompat.getColor(this, R.color.colorPrimaryTranslucent))
.strokeColor(ContextCompat.getColor(this, R.color.colorPrimary))
)
}
يتم ضبط الخاصية circle
بحيث تتم إزالة الدائرة السابقة وإضافة علامة جديدة عند النقر على محدِّد جديد. لاحظ أن واجهة برمجة التطبيقات لإضافة دائرة تشبه إلى حد كبير إضافة علامة.
- واصِل تشغيل التطبيق الآن للاطّلاع على التغييرات.
10- التحكّم بالكاميرا
بالنسبة إلى المهمة الأخيرة، يمكنك الاطّلاع على بعض عناصر التحكّم في الكاميرا حتى تتمكن من التركيز على منطقة معيّنة.
الكاميرا والعرض
إذا لاحظت عند تشغيل التطبيق، تعرض الكاميرا قارة أفريقيا، وتحتاج إلى التحرك والتكبير بشكل متواصل إلى سان فرانسيسكو للعثور على العلامات التي أضفتها. على الرغم من أن ذلك يمكن أن يكون وسيلة ممتعة لاستكشاف العالم، فإنه ليس مفيدًا إذا كنت ترغب في عرض العلامات على الفور.
للمساعدة في ذلك، يمكنك ضبط موضع الكاميرا آليًا بحيث يتم توسيط العرض في المكان الذي تريده.
- يمكنك إضافة الرمز التالي إلى مكالمة
getMapAsync()
لضبط وضع عرض الكاميرا بحيث يتم إعداده في سان فرانسيسكو عند تشغيل التطبيق.
MainActivity.onCreate()
mapFragment?.getMapAsync { googleMap ->
// Ensure all places are visible in the map.
googleMap.setOnMapLoadedCallback {
val bounds = LatLngBounds.builder()
places.forEach { bounds.include(it.latLng) }
googleMap.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds.build(), 20))
}
}
يتم استدعاء الخاصية setOnMapLoadedCallback()
أولاً حتى يتم تنفيذ تحديث الكاميرا فقط بعد تحميل الخريطة. وهذه الخطوة ضرورية لأنه يجب احتساب خصائص الخريطة، مثل الأبعاد، قبل إجراء مكالمة تحديث الكاميرا.
في لامدا، يتم إنشاء كائن LatLngBounds
جديد، يحدد منطقة مستطيلة على الخريطة. يتم إنشاء هذا تدريجيًا من خلال تضمين جميع قيم المكان LatLng
فيه لضمان أن جميع الأماكن داخل الحدود. بعد إنشاء هذا الكائن، يتم استدعاء طريقة moveCamera()
على GoogleMap
ويتم تقديم CameraUpdate
إليه من خلال CameraUpdateFactory.newLatLngBounds(bounds.build(), 20)
.
- شغِّل التطبيق ولاحظ أن الكاميرا تبدأ الآن في شرم الشيخ.
جارٍ الاستماع إلى تغييرات الكاميرا
بالإضافة إلى تعديل موضع الكاميرا، يمكنك أيضًا الاستماع إلى تحديثات الكاميرا أثناء تحرك المستخدم في جميع أنحاء الخريطة. قد يكون هذا مفيدًا إذا أردت تعديل واجهة المستخدم أثناء تحرك الكاميرا.
للترفيه فقط، يمكنك تعديل الرمز لجعل العلامات شفّافة كلما تم تحريك الكاميرا.
- في الطريقة
addClusteredMarkers()
، استمر في إضافة الأسطر التالية باتجاه الجزء السفلي من الطريقة:
MainActivity.addClusteredMarkers()
// When the camera starts moving, change the alpha value of the marker to translucent.
googleMap.setOnCameraMoveStartedListener {
clusterManager.markerCollection.markers.forEach { it.alpha = 0.3f }
clusterManager.clusterMarkerCollection.markers.forEach { it.alpha = 0.3f }
}
يؤدي ذلك إلى إضافة OnCameraMoveStartedListener
بحيث كلما بدأت الكاميرا في الحركة، تم تعديل كل علامات الإصدار'؛ (كل من المجموعات والعلامات) إلى 0.3f
بحيث تبدو العلامات شبه شفافة.
- أخيرًا، لتعديل العلامات الشفافة مرة أخرى على التعتيم عندما تتوقف الكاميرا، عدِّل محتوى
setOnCameraIdleListener
في طريقةaddClusteredMarkers()
على النحو التالي:
MainActivity.addClusteredMarkers()
googleMap.setOnCameraIdleListener {
// When the camera stops moving, change the alpha value back to opaque.
clusterManager.markerCollection.markers.forEach { it.alpha = 1.0f }
clusterManager.clusterMarkerCollection.markers.forEach { it.alpha = 1.0f }
// Call clusterManager.onCameraIdle() when the camera stops moving so that reclustering
// can be performed when the camera stops moving.
clusterManager.onCameraIdle()
}
- واصِل تشغيل التطبيق للاطّلاع على النتائج.
11- خرائط KTX
بالنسبة إلى تطبيقات Kotlin التي تستخدم حزمة تطوير برامج (SDK) واحدة أو أكثر لنظام التشغيل Android على "منصة خرائط Google"، تتوفّر إضافة Kotlin أو مكتبات KTX لمساعدتك في الاستفادة من ميزات لغة Kotlin، مثل الكوروتينات وخصائص الإضافات/وظائفها والمزيد. لكل حِزمة تطوير برامج (SDK) في "خرائط Google" مكتبة KTX مطابقة كما هو موضّح أدناه:
في هذه المَهمّة، ستستخدم مكتبات KTX للخرائط و"خرائط Utils لـ KTX" في تطبيقك كما ستعيد تنفيذ المهام السابقة'؛ وعمليات التنفيذ بحيث يمكنك استخدام ميزات اللغة الخاصة بلغة Kotlin في تطبيقك.
- تضمين تبعيات KTX في ملف build.gradle على مستوى التطبيق
ونظرًا لاستخدام التطبيق لكلٍّ من حِزمة تطوير البرامج (SDK) لخدمة "خرائط Google" لنظام التشغيل Android وحزمة تطوير برامج "خرائط Google" لمكتبة برامج خدمات Android، عليك تضمين مكتبات KTX المقابلة لهذه المكتبات. ستستخدم أيضًا ميزة متوفّرة في مكتبة"دورة حياة KTX"في AndroidX في هذه المهمة، لذا أدرِج هذه الاعتمادية أيضًا في ملف build.gradle
على مستوى التطبيق.
build.gradle
dependencies {
// ...
// Maps SDK for Android KTX Library
implementation 'com.google.maps.android:maps-ktx:3.0.0'
// Maps SDK for Android Utility Library KTX Library
implementation 'com.google.maps.android:maps-utils-ktx:3.0.0'
// Lifecycle Runtime KTX Library
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
}
- استخدام دالّة الإضافة GoogleMap.addMarker() وGoogleMap.addCircle()
توفر مكتبة Maps KTX بديل لـ DSL API لـ GoogleMap.addMarker(MarkerOptions)
وGoogleMap.addCircle(CircleOptions)
المستخدمَين في الخطوات السابقة. لاستخدام واجهات برمجة التطبيقات المذكورة أعلاه، من الضروري إنشاء فئة تحتوي على خيارات لعلامة أو دائرة، بينما من خلال بدائل KTX، يمكنك إعداد خيارات العلامة أو الدائرة في lambda التي تقدمها.
لاستخدام واجهات برمجة التطبيقات هذه، عدِّل طريقتَي MainActivity.addMarkers(GoogleMap)
وMainActivity.addCircle(GoogleMap)
:
MainActivity.addMarkers(GoogleMap)
/**
* Adds markers to the map. These markers won't be clustered.
*/
private fun addMarkers(googleMap: GoogleMap) {
places.forEach { place ->
val marker = googleMap.addMarker {
title(place.name)
position(place.latLng)
icon(bicycleIcon)
}
// Set place as the tag on the marker object so it can be referenced within
// MarkerInfoWindowAdapter
marker.tag = place
}
}
MainActivity.addCircle(GoogleMap)
/**
* Adds a [Circle] around the provided [item]
*/
private fun addCircle(googleMap: GoogleMap, item: Place) {
circle?.remove()
circle = googleMap.addCircle {
center(item.latLng)
radius(1000.0)
fillColor(ContextCompat.getColor(this@MainActivity, R.color.colorPrimaryTranslucent))
strokeColor(ContextCompat.getColor(this@MainActivity, R.color.colorPrimary))
}
}
يُرجى العِلم أنّ إعادة كتابة الطرق المذكورة أعلاه بهذه الطريقة تكون أكثر إيجازًا ووضوحًا، ما يجعلها ممكنة باستخدام دالة Kotlin's مع المُستقبِل.
- استخدِم دالّتَي التعليق على الإضافة MapMapFragment.aانتظارMap() وGoogleMap.aWayMapLoad().
وتوفّر مكتبة KTX للخرائط أيضًا إضافات وظائف معلّقة لاستخدامها في الكوروتين. على وجه التحديد، هناك بدائل مرتبطة بوظائف SupportMapFragment.getMapAsync(OnMapReadyCallback)
وGoogleMap.setOnMapLoadedCallback(OnMapLoadedCallback)
. وباستخدام واجهات برمجة التطبيقات البديلة هذه، لا تكون هناك حاجة لتمرير استدعاءات، بدلاً من ذلك، السماح لك بتلقي استجابة هذه الطرق بطريقة تسلسلية ومتزامنة.
بما أن هذه الطرق تعمل على تعليق الوظائف، يلزم استخدامها في الكوروتين. توفّر مكتبة وقت تشغيل دورة حياة KTX امتدادًا لتوفير نطاقات الكوروتين الواعية بدورة الحياة بحيث يتم تشغيل الكوروتينات في حدث دورة الحياة المناسب وإيقافها.
عند الجمع بين هذه المفاهيم، يمكنك تعديل طريقة MainActivity.onCreate(Bundle)
:
MainActivity.onCreate(حزمة)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val mapFragment =
supportFragmentManager.findFragmentById(R.id.map_fragment) as SupportMapFragment
lifecycleScope.launchWhenCreated {
// Get map
val googleMap = mapFragment.awaitMap()
// Wait for map to finish loading
googleMap.awaitMapLoad()
// Ensure all places are visible in the map
val bounds = LatLngBounds.builder()
places.forEach { bounds.include(it.latLng) }
googleMap.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds.build(), 20))
addClusteredMarkers(googleMap)
}
}
ينفّذ نطاق كورتين lifecycleScope.launchWhenCreated
على مستوى المجموعة عندما يكون النشاط على الأقل في الحالة التي تم إنشاؤها. تجدر الإشارة أيضًا إلى أنه تم استبدال طلبات استرداد العنصر GoogleMap
وانتظار انتهاء تحميل الخريطة بـ SupportMapFragment.awaitMap()
وGoogleMap.awaitMapLoad()
على التوالي. تتيح لك إعادة إنشاء الرمز باستخدام دوال التعليق هذه كتابة الرمز المكافئ المستند إلى رد الاتصال بطريقة تسلسلية.
- هيّا، أعِد تصميم تطبيقك باستخدام التغييرات التي أصلحتها.
12- تهانينا
تهانينا. لقد تناولت الكثير من المحتوى ونأمل أن تكون على دراية أفضل بالميزات الأساسية المتوفرة في حزمة تطوير البرامج (SDK) لتطبيق "خرائط Google" لنظام التشغيل Android.
مزيد من المعلومات
- الأماكن المخصصة لنظام التشغيل Android: يمكنك الاطّلاع على مجموعة بيانات وافية عن الأماكن لاستكشاف الأنشطة التجارية القريبة منك.
- android-maps-ktx: مكتبة مفتوحة المصدر تسمح لك بالدمج مع Maps SDK لنظام التشغيل Android وMaps لـ Android Utility ومكتبة متوافقة مع لغة Kotlin.
- android-place-ktx: مكتبة مفتوحة المصدر تسمح لك بالدمج مع Places SDK لنظام التشغيل Android بطريقة متوافقة مع لغة Kotlin.
- android-samples: نموذج رمز على GitHub يوضّح جميع الميزات المشمولة في هذا الدرس التطبيقي حول الترميز والمزيد.
- المزيد من الدروس التطبيقية حول ترميز Kotlin لإنشاء تطبيقات متوافقة مع Android باستخدام "منصة خرائط Google"