نقشه سیدار مپ و API های آن

آموزش نقشه سیدار مپ (CedarMaps) در برنامه نویسی اندروید

در جلسه گذشته به نحوه نمایش نقشه Google Maps در اندروید پرداختیم. به دلیل محدودیت‌هایی که به واسطه پرداخت ارزی جهت استفاده از API های گوگل مپ برای توسعه دهندگان و برنامه نویسان اندرویدی داخل ایران وجود دارد تصمیم گرفتیم نحوه کار با یکی از سرویس دهنده‌های نقشه ایرانی را آموزش دهیم. در این جلسه و جلسات بعد به آموزش استفاده از نقشه سیدار مپ (CedarMaps) در اندروید می‌پردازیم.

آنچه در این آموزش می‌خوانید:

  • معرفی نقشه سیدار مپ و API های آن
  • نحوه اضافه کردن آنلاین و آفلاین کتابخانه SDK سیدار مپ در اندروید استودیو و رفع مشکل احتمالی
  • مجوز یا Permission های مورد نیاز برای استفاده از سیدار مپ و API ها
  • نحوه استفاده از ClientID و ClientSecret دریافتی از پشتیبانی سیدار مپ
  • استفاده از منوی BottomNavigationView برای نمایش فرگمنت‌ها
  • دریافت مجوز دسترسی به موقعیت مکانی توسط متد MapBox
  • پیاده سازی API مکان یابی و نمایش نقشه
  • پیاده سازی API نقطه یابی (Reverse Geocoding) و تبدیل نقطه به آدرس
  • پیاده سازی API جستجو بر اساس نام مکان (Forward Geocoding)
  • پیاده سازی API مسیریابی و تخمین مسافت (Direction)

تصاویر نهایی پروژه:

نمایش نقشه سیدار مپ در برنامه نویسی اندروید و یافتن موقعیت مکانی کاربر روی نقشه
تبدیل نقطه جغرافیایی به آدرس در نقشه سیدار مپ
جستجوی نقاط جغرافیایی بر اساس نام اماکن مانند شهر، منطقه، نام خیابان و... در CedarMaps
مسیریابی و تخمین مسافت در نقشه سیدار مپ در برنامه نویسی اندروید

 

این جلسه در قالب PDF و در ۶۹ صفحه تهیه شده که در ادامه چند صفحه‌ ابتدایی را مشاهده می‌کنید:

چرا سیدار مپ؟

اگر آموزش جلسه قبل را مطالعه کرده باشید گفتیم که مدتیست گوگل سیاست خود در قبال ارائه API های مرتبط با Google Map را تغییر داده و برای استفاده از پلن رایگان و محدود هم نیاز به احراز هویت از طریق Billing دارد که تهیه آن برای اکثر برنامه نویسان و توسعه دهندگان داخل ایران ممکن نیست. ضمن اینکه هر لحظه ممکن است تحریم‌های جدیدی روی این سرویس‌ها اعمال شده و به عنوان مثال حتی امکان استفاده رایگان از SDK نقشه هم وجود نداشته باشد. بنابراین انتخاب منطقی این است که از سایر گزینه‌های روی میز استفاده کنیم!
سرویس‌های داخلی متعددی در حال حاضر بر سر ارائه API نقشه به رقابت با یکدیگر پرداخته‌اند که می‌توان به سیدار مپ، نشان و مپ اشاره کرد. هرکدام از این سرویس‌ها SDK مربوط به اندروید را در اختیار توسعه دهندگان قرار داده‌اند که با اضافه کردن آن به پروژه می‌توان از API ها و قابلیت‌های مختلف از جمله نمایش موقعیت مکانی روی نقشه، مکان یابی، مسیریابی و تبدیل نقطه جغرافیایی به آدرس استفاده کرد.
هرکدام از این سرویس دهنده‌ها مزایا و معایبی داشته و علاوه بر آن در نحوه تعرفه گذاری نیز تفاوت‌هایی دارند.

تعرفه‌های نقشه سیدار مپ

تصویر فوق مربوط به تعرفه‌های سیدار مپ در زمان تهیه این آموزش است. پلن رایگان آن شامل ۲۰۰۰ API Call در روز است. یعنی تا ۲۰۰۰ درخواست از سمت کاربران برنامه ما بطور رایگان پاسخ داده می‌شود. این درخواست‌ها شامل نمایش موقعیت مکانی کاربر، تبدیل موقعیت جغرافیایی به آدرس (شامل استان، شهر، منطقه، خیابان، کوچه و…)، مسیریابی و تخمین مسافت خواهد بود که توسط کاربران برنامه ارسال می‌شود.
همچنین در پلن رایگان نمایش و لود نقشه تا سقف ۱۰۰۰۰ مرتبه در روز امکان پذیر است. بنابراین واضح است که صرف نمایش نقشه روی برنامه، یک API Call محسوب نمی‌شود و فقط مصرف پهنای باند را در پی خواهد داشت که در این پلن تا ۱۰۰ گیگابایت در ماه تعیین شده.
با توجه به مشخصات پلن رایگان CedarMap می‌توان گفت برای اکثر اپلیکیشن‌های با مخاطب کم، همین پلن کفایت کرده و حداقل برای ابتدای کار نیازی به ارتقاء به پلن‌های بالاتر و پرداخت هزینه نیست. برای استفاده از پلن رایگان باید درخواست خود را در قسمت مربوطه داخل سایت سیدار مپ ارسال کنید. معمولا در کمتر از ۲۴ ساعت اطلاعات مورد نیاز برای استفاده از نقشه به ایمیل شما ارسال خواهد شد. این اطلاعات شامل یک Client ID و Client Secret است که باید در پروژه تعریف شود.
اگر صفحه نخست وب سایت سیدار مپ را بررسی کرده باشید عمده تمرکز آن روی مقایسه نقشه گوگل و سیدار مپ است. از جمله خوانایی و زیبایی، سرعت، جزئیات نقشه، دقت بیشتر در مسیریابی و… که در تمام موارد برتری سیدار مپ نتیجه گرفته شده. البته که بنده تمام این برتری‌ها را تایید نمی‌کنم و در برخی موارد اغراق شده. حتی در تست‌ها مشخص شد در برخی موارد از جمله تبدیل موقعیت جغرافیایی به آدرس (بخصوص در مناطقی غیر از کلان شهرها) سیدار مپ از دقت کمتری برخوردار بوده و هنوز تا رسیدن به حد مطلوب جای کار دارد.

خوانایی نقشه سیدار مپ

سرعت بالای لود نقشه CedarMaps

جزئیات نقشه سیدار مپ

API تبدیل نقطه به آدرس نقشه سیدار مپ

API جستجو در معابر سیدار مپ

API مسیریابی و تخمین مسافت سیدار مپ

با اینحال از نظر بنده و در مقایسه‌ای که بین چند سرویس دهنده داخلی انجام دادم در نهایت این سرویس را انتخاب کردم.

پیاده سازی نقشه سیدار مپ در اپلیکیشن اندرویدی

در صفحه مستندات نقشه سیدار مپ اطلاعات مربوط به نحوه پیاده سازی نقشه و API های آن در اندروید، iOS و وب در دسترس توسعه دهندگان قرار گرفته است:

مستندات API های نقشه سیدار مپ

با کلیک روی گزینه Android به صفحه SDK سیدار مپ برای پلتفرم اندروید در گیت هاب منتقل می‌شوم:

https://github.com/cedarstudios/cedarmaps-android-sdk

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

ساخت پروژه اندرویدی سیدار مپ

در این پروژه قصد داریم در قالب چند فرگمنت، امکانات و API های مختلف نقشه سیدار مپ را آموزش دهیم. طبق آموزش ساخت پروژه در اندروید استودیو یک پروژه با نام CedarMap با یک Empty Activity و زبان Java ایجاد می‌کنم.
ابتدا مطابق توضیحات موجود در صفحه گیت هاب سیدار مپ اندروید، هردو فایل build.gradle سطح project و app را ویرایش می‌کنم. در قدم نخست و در build.gradle سطح پروژه، آدرس مخزن سیدار مپ را در بلاک repositories زیرمجموعه بلاک allprojects اضافه می‌کنم:

build.gradle (Project)

buildscript {
    repositories {
        google()
        jcenter()
        
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.5.3'
        
    }
}

allprojects {
    repositories {
        jcenter()
        google()

        maven {
            url "https://repo.cedarmaps.com/android/"
        }
        
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

در قدم بعد فایل build.gradle سطح app را ویرایش می‌کنم. برای جلوگیری از بروز خطای مربوط به نسخه جاوا، مطابق توضیحات موجود در صفحه گیت هاب سیدار مپ یک بلاک با نام compileOptions به بلاک android باید اضافه شود:

build.gradle (app)

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.3"
    defaultConfig {
        applicationId "ir.android_studio.cedarmap"
        minSdkVersion 19
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

در ادامه کتابخانه‌های مورد نیاز برای نقشه را در بلاک dependencies اضافه و پروژه را سینک می‌کنم:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'com.cedarmaps:CedarMapsSDK:4.2.1'
    implementation 'com.mapbox.mapboxsdk:mapbox-android-plugin-annotation-v7:0.6.0'
    implementation 'com.google.android.material:material:1.2.0-alpha04'
    implementation 'androidx.vectordrawable:vectordrawable:1.1.0'
}

کتابخانه اصلی و ضروری سیدار مپ مورد اول یعنی CedarMapsSDK است که مربوط به SDK نقشه می‌باشد. ۳ مورد دیگر به ترتیب برای استفاده از سایر امکانات SDK مپ باکس، اضافه کردن دکمه FAB و Bottom Navigation و ترسیم تصاویر وکتوری تعریف شده‌اند.

تذکر: به دلیل اینکه SDK سیدار مپ از سرور اختصاصی سیدار به نشانی https://repo.cedarmaps.com/android دریافت می‌شود ممکن است در دریافت کتابخانه و سینک شدن پروژه با مشکل مواجه شوید. ابتدا مطمئن شوید ابزار تغییر IP شما از نوعی است که محدود به دامنه‌های خاص نبوده و همه url ها را می‌تواند از درگاه خود عبور دهد. در غیر اینصورت امکان اتصال به سرور سیدار میسر نخواهد بود. سرویس‌های تغییر آی‌پی مانند “FOD” و “شکن” روی دامنه‌های محدودی تنظیم شده و در این مورد قابل استفاده نیستند. برای انتخاب ابزار مناسب، صفحه نحوه فعال کردن پروکسی در اندروید استودیو را مطالعه نمائید.
البته ممکن است با رعایت نکات فوق باز هم موفق به سینک کردن پروژه نشوید. مانند آنچه برای من اتفاق افتاد! مکاتبات زیادی هم با بخش پشتیبانی سیدار مپ داشتم اما ظاهرا نه سرور ایرادی دارد نه اندروید استودیو من. بنابراین تنها پیشنهادی که ارائه شد اضافه کردن آفلاین کتابخانه CedarMapsSDK بود که لینک دانلود آن را در ایمیل ارسال کردند.
نحوه افزودن آفلاین کتابخانه‌ها در اندروید استودیو را قبلا در مطلب اکتیویتی‌ها در اندروید توضیح داده‌ام. ابتدا فایل .jar یا .aar کتابخانه در پوشه app>libs قرار می‌گیرد:

اضافه کردن کتابخانه SDK نقشه سیدار مپ به صورت آفلاین

SDK سیدار مپ از سه کتابخانه دیگر استفاده می‌کند. بنابراین لازم است در حالت نصب آفلاین کتابخانه سیدار مپ، این ۳ کتابخانه را جداگانه تعریف کنیم که به ترتیب mapbox-android-sdk، okhttp و gson هستند. در نهایت لیست کتابخانه‌ها به اینصورت است:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation files('libs/CedarMapsSDK-4.2.1.aar')
    api 'com.mapbox.mapboxsdk:mapbox-android-sdk:8.4.0'
    implementation 'com.squareup.okhttp3:okhttp:3.12.6'
    implementation 'com.google.code.gson:gson:2.8.6'
    implementation 'com.mapbox.mapboxsdk:mapbox-android-plugin-annotation-v7:0.6.0'
    implementation 'com.google.android.material:material:1.2.0-alpha04'
    implementation 'androidx.vectordrawable:vectordrawable:1.1.0'
}

حالا پروژه را سینک می‌کنم. در حالت آفلاین از ابزارهای تغییر آی‌پی مانند FOD هم می‌توان استفاده کرد زیرا دیگر نیازی به اتصال به سرور سیدار مپ نداریم.
در صورتی که نسخه جدیدتری از CedarMapsSDK منتشر شده بود برای دریافت فایل نسخه جدید به پشتیبانی سیدار مپ ایمیل زده و یا ورژن را در لینک زیر جایگزین کنید:

https://repo.cedarmaps.com/android/com/cedarmaps/CedarMapsSDK/4.2.1/CedarMapsSDK-4.2.1.aar

حالا پروژه را سینک می‌کنم.

در مرحله بعد باید دسترسی‌های مورد نیاز نقشه سیدار مپ را در مانیفست پروژه تعریف کنیم که در مطلب پیاده سازی Runtime Permission در اندروید به طور مفصل با پرمیشن‌ها آشنا شدیم.

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="ir.android_studio.cedarmap">

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

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

نکته: اگر هنگام لود نقشه‌های آنلاین دقت کرده باشید نقشه به صورت قسمت‌های مربعی که کنار یکدیگر قرار می‌گیرند بارگزاری می‌شود که به هرکدام از این قسمت‌ها یک تایل (Tile به معنی کاشی) گفته می‌شود.

در توضیحات صفحه گیت هاب قید شده که ClientID و ClientSecret بهتر است در یک subclass از کلاس Application پروژه تعریف شود. بنابراین یک کلاس با نام دلخواه CedarID به پروژه اضافه کرده و آنرا از کلاس Application ارث بری می‌کنم. سپس متد onCreate() را به کلاس اضافه و کد مربوطه را درون متد قرار می‌دهم:

CedarID.java

package ir.android_studio.cedarmap;

import android.app.Application;

import com.cedarstudios.cedarmapssdk.CedarMaps;
import com.cedarstudios.cedarmapssdk.model.MapID;

public class CedarID extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        CedarMaps.getInstance()
                .setClientID("YOUR_CLIENT_ID")
                .setClientSecret("YOUR_CLIENT_SECRET")
                .setContext(this)
                .setMapID(MapID.MIX);
    }
    
}

کافیست در کد فوق، دو کد دریافتی از سیدار مپ را جایگزین کنید. توجه داشته باشید subclass کلاس Application باید درون مانیفست تعریف شود. برای اینکار کافیست ویژگی name به تگ application مانیفست اضافه شود که مقدار آن، نام کلاس است:

<application
    android:name=".CedarID"
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity android:name=".MainActivity">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />

            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
</application>

در layout اکتیویتی یک FrameLayout برای نمایش فرگمنت‌ها و یک BottomNavigationView‌ برای نمایش منوی پایین صفحه تعریف می‌کنم:

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <FrameLayout
        android:id="@+id/content"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">

    </FrameLayout>

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/navigationView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:background="?android:attr/windowBackground"
        app:labelVisibilityMode="labeled"
        app:menu="@menu/navigation" />

</LinearLayout>

منوی BottomNavigation شامل ۴ آیتم است که با نام navigation.xml و در قالب یک menu در پروژه تعریف شده:

navigation.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

    <item
        android:id="@+id/navigation_map"
        android:icon="@drawable/ic_map_marker"
        android:title="نقشه" />

    <item
        android:id="@+id/navigation_reverse"
        android:icon="@drawable/ic_reverse_geocode"
        android:title="نقطه یابی" />

    <item
        android:id="@+id/navigation_forward"
        android:icon="@drawable/ic_forward_geocode"
        android:title="جستجو" />

    <item
        android:id="@+id/navigation_direction"
        android:icon="@drawable/ic_direction"
        android:title="مسیریابی" />

</menu>
نکته: ساختار نهایی پروژه در صفحه ۶۰ نمایش داده شده است.

حالا در اکتیویتی کدهای لازم را اضافه می‌کنم:

package ir.android_studio.cedarmap;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;

import com.google.android.material.bottomnavigation.BottomNavigationView;


public class MainActivity extends AppCompatActivity implements BottomNavigationView.OnNavigationItemSelectedListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

ابتدا کلاس اکتیویتی را implement می‌کنم به BottomNavigationView.OnNavigationItemSelectedListener. سپس با قرار دادن نشانگر موس روی بدنه کلاس و زدن alt + enter متد onNavigationItemSelected را به کلاس اضافه می‌کنم:

اضافه کردن متد onNavigationItemSelected برای مدیریت API های نقشه سیدار مپ

MainActivity.java

package ir.android_studio.cedarmap;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.MenuItem;

import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.mapbox.mapboxsdk.geometry.LatLng;


public class MainActivity extends AppCompatActivity implements BottomNavigationView.OnNavigationItemSelectedListener {

    public static final LatLng VANAK_SQUARE = new LatLng(35.7572, 51.4099);
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        BottomNavigationView navigation = findViewById(R.id.navigationView);
        navigation.setOnNavigationItemSelectedListener(MainActivity.this);
        navigation.setSelectedItemId(R.id.navigation_map);

    }

    @Override
    public boolean onNavigationItemSelected(@NonNull MenuItem item) {
        return false;
    }
}

یک LatLng با نام VANAK_SQUARE تعریف شده که مختصات میدان ونک تهران در آن ذخیره شده. از این نقطه برای نمایش محل پیش فرض نقشه استفاده خواهیم کرد.
درون متد onCreate() اکتیویتی ویجت BottomNavigation را تعریف کرده و همچنین مشخص کردم آیتم با شناسه navigation_map به صورت پیش فرض و هنگام اجرای اپلیکیشن انتخاب شده باشد که مربوط به فرگمنت نمایش نقشه و موقعیت مکانی فرد خواهد بود.
متد onNavigationItemSelected را به صورت زیر تکمیل می‌کنم:

@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
    Fragment fragment = null;
    switch (item.getItemId()) {
        case R.id.navigation_map:
            setTitle("نقشه");
            fragment = new MapFragment();
            break;
        case R.id.navigation_reverse:
            setTitle("نقطه یابی");
            fragment = new ReverseGeocodeFragment();
            break;
        case R.id.navigation_forward:
            setTitle("");
            fragment = new ForwardGeocodeFragment();
            break;
        case R.id.navigation_direction:
            setTitle("مسیریابی");
            fragment = new DirectionFragment();
            break;
    }

    if (fragment != null) {
        FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
        if (getSupportFragmentManager().getFragments().isEmpty()) {
            transaction.add(R.id.content, fragment, String.format(Locale.US, "item: %d", item.getItemId())).commit();
        } else {
            transaction.replace(R.id.content, fragment, String.format(Locale.US, "item: %d", item.getItemId())).commit();
            invalidateOptionsMenu();
        }
        return true;
    }

    return false;
}

با لمس هریک از آیتم‌های منو، فرگمنت مربوطه اجرا خواهد شد.
دسترسی به موقعیت جغرافیایی کاربر جزء مجوزهای خطرناک (Dangerous Permissions) بحساب می‌آید بنابراین طبق مبحث Runtime Permission باید برای اندروید ۶ و به بالا هنگام اجرای برنامه مجوز مربوطه را از کاربر دریافت کنیم. برای اینکار متد onRequestPermissionsResult را در کلاس اکتیویتی Override می‌کنم:
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}

متد را به صورت زیر تکمیل می‌کنم:

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {

    List allFragments = getSupportFragmentManager().getFragments();
    if (allFragments.isEmpty()) {
        return;
    }

    Fragment currentFragment = (Fragment) allFragments.get(allFragments.size() - 1);
    if (currentFragment instanceof PermissionsListener) {
        currentFragment.onRequestPermissionsResult(requestCode, permissions, grantResults);
        return;
    }

    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}

ابتدا بررسی می‌شود آیا فرگمنتی وجود دارد یا نه. در صورتی که جواب منفی بود (یعنی allFragments برابر با isEmpty باشد) هیچ کاری انجام نشده و return می‌شود. در ادامه شرطی را تعریف می‌کنم تا صرفا هنگامی مجوز دسترسی به موقعیت مکانی از کاربر دریافت شود که به آن نیاز داشته باشیم. به عنوان مثال در API جستجو بر اساس نام مکان و یا مسیریابی، نیازی به موقعیت کاربر نیست و چنانچه شخص صرفا از این قابلیت استفاده کند منطقی نیست دسترسی موقعیت را درخواست کنیم.
ابتدا فرگمنت در حال اجرا در currentFragment قرار می‌گیرد. سپس بررسی می‌کنیم اگر از فرگمنت موجود در currentFragment درخواست مجوز دسترسی ارسال شده، پیغام مربوطه را به کاربر نمایش بده تا دسترسی را تایید یا لغو کند. در اینجا خبری از کدهای طولانی Runtime Permission پیش فرض اندروید نیست و مدیریت اینکار توسط اینترفیس PermissionsListener انجام می‌شود که مربوط به MapBox است. در واقع MapBox توسعه دهنده را از نوشتن کدهای اضافی بی نیاز کرده. البته استفاده از این قابلیت مپ باکس الزامی نبوده و می‌توان به صورت عادی و طبق آنچه در مبحث مربوط به Runtime Permission تمرین کرده بودیم مجوز دسترسی به موقعیت جغرافیایی را هنگام اجرای برنامه یا هنگام اجرای فرگمنت مدنظر از کاربر دریافت کنیم.

if (currentFragment instanceof PermissionsListener)

در خط فوق بررسی می‌شود آیا فرگمنت ذخیره شده در متغیر currentFragment از جنس PermissionsListener هست یا نه. به عبارت دیگر بررسی می‌شود آیا در فرگمنت فعلی مجوزی نیاز هست که مربوط به MapBox باشد یا خیر. به طور خلاصه دستور instanceof در جاوا وظیفه برگرداندن نتیجه مقایسه جنس دو آیتم را بر عهده دارد.
کد نهایی اکتیویتی:

MainActivity.java

package ir.android_studio.cedarmap;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction;

import android.os.Bundle;
import android.view.MenuItem;

import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.mapbox.android.core.permissions.PermissionsListener;
import com.mapbox.mapboxsdk.geometry.LatLng;

import java.util.List;
import java.util.Locale;


public class MainActivity extends AppCompatActivity implements BottomNavigationView.OnNavigationItemSelectedListener {

    public static final LatLng VANAK_SQUARE = new LatLng(35.7572, 51.4099);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        BottomNavigationView navigation = findViewById(R.id.navigationView);
        navigation.setOnNavigationItemSelectedListener(MainActivity.this);
        navigation.setSelectedItemId(R.id.navigation_map);

    }

    @Override
    public boolean onNavigationItemSelected(@NonNull MenuItem item) {
        Fragment fragment = null;
        switch (item.getItemId()) {
            case R.id.navigation_map:
                setTitle("نقشه");
                fragment = new MapFragment();
                break;
            case R.id.navigation_reverse:
                setTitle("نقطه یابی");
                fragment = new ReverseGeocodeFragment();
                break;
            case R.id.navigation_forward:
                setTitle("");
                fragment = new ForwardGeocodeFragment();
                break;
            case R.id.navigation_direction:
                setTitle("مسیریابی");
                fragment = new DirectionFragment();
                break;
        }

        if (fragment != null) {
            FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
            if (getSupportFragmentManager().getFragments().isEmpty()) {
                transaction.add(R.id.content, fragment, String.format(Locale.US, "item: %d", item.getItemId())).commit();
            } else {
                transaction.replace(R.id.content, fragment, String.format(Locale.US, "item: %d", item.getItemId())).commit();
                invalidateOptionsMenu();
            }
            return true;
        }

        return false;
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {

        List allFragments = getSupportFragmentManager().getFragments();
        if (allFragments.isEmpty()) {
            return;
        }

        Fragment currentFragment = (Fragment) allFragments.get(allFragments.size() - 1);
        if (currentFragment instanceof PermissionsListener) {
            currentFragment.onRequestPermissionsResult(requestCode, permissions, grantResults);
            return;
        }

        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }

}

جهت مطالعه ادامه آموزش، فایل PDF را دانلود نمائید

توجه : سورس پروژه درون پوشه Exercises قرار دارد

با توجه به اینکه آموزش‌های پایه با قیمت پایین در اختیار کاربر قرار گرفته و درآمد حاصل صرف تامین هزینه‌های وب سایت و تهیه آموزش‌های آتی می‌شود، به اشتراک گذاری این فایل با دیگران خلاف اخلاق است.

دانلود نسخه کامل این آموزش به همراه سورس پروژه
تعداد صفحات : ۶۹
حجم : ۳ مگابایت
قیمت : ۳۶ هزار تومان
توجه: صرفا در صورتی از درگاه پشتیبان استفاده کنید که قادر به پرداخت از طریق سبد دانلود نباشید.
افزودن به سبد دانلود درگاه پشتیبان
این مطلب چقدر برایتان مفید بود؟ لطفا امتیاز دهید
4.3/5 - (6 امتیاز)
پرسش‌ها و دیدگاه‌های کاربران
دوره آموزش برنامه نویسی اندروید
دوره آموزش برنامه نویسی اندروید

با دریافت این دوره به تمامی آموزش‌های غیر رایگان و رایگان موجود در وب سایت دسترسی دارید که تخفیفی برای آموزش‌های غیر رایگان نیز درنظر گرفته شده. این پکیج به دو صورت دانلودی و ارسال پستی ارائه می‌گردد.
آموزش‌های اندروید استودیو در دو دسته «پایه» و «تکمیلی» منتشر می‌شوند.
آموزش‌های پایه شامل مباحث اصلی و ضروری و آموزش‌های تکمیلی مطالبی است که می‌بایست در کنار مطالب اصلی بررسی شود.
با خرید این دوره، به تمامی آموزش‌های غیر رایگانی که در آینده منتشر می‌شود نیز به صورت رایگان دسترسی خواهید داشت!

یک دیدگاه بنویسید

پرسش‌های زیر تایید و پاسخ داده نـــخواهند شد:
۱: جزء موارد مطرح شده در صفحات مشکلات و پرسش‌های رایج و بروزرسانی‌های محتوای آموزشی باشد
۲: سوال قبلا توسط کاربران در دیدگاه‌ها مطرح و پاسخ داده شده باشد
۳: پرسش خارج از مبحث آموزشی موجود در این صفحه باشد