Skip to content

Retrofit

Reza Kabiri edited this page Sep 21, 2020 · 9 revisions

پروژه‌ی آموزشی نحوه‌ استفاده از SDK نقشه نشان

Retrofit

با استفاده از REST API می‌توانیم از وب‌سرویس‌های ارائه شده توسط زیرساخت نقشه نشان استفاده کنیم. لیست این وب‌سرویس‌ها و ساختار درخواست و پاسخی که از سرور دریافت می‌شود در لینک زیر آمده است:

وب‌سرویس‌های نشان

برای استفاده از REST API در اندروید کتابخانه‌های مختلفی وجود دارد، که در این بخش، به معرفی و نحوه استفاده از آن برای دریافت آدرس یک موقعیت جغرافیایی توسط وب‌سرویس «تبدیل موقعیت به آدرس» خواهیم پرداخت.

توضیح نحوه استفاده از این وب‌سرویس را می‌توانید از اینجا مطالعه کنید.

اگر بخواهیم آدرس موقعیت جغرافیایی زیر را به دست آوریم:

35.702918, 51.340880

درخواست ما باید از نوع GET و با آدرس زیر باشد:

پاسخ دریافت شده از سمت سرور:

{
	"neighbourhood": "طرشت",
	"address": "عزیزی، نزدیک به سلطان محمدی",
	"municipality_zone": "2",
	"in_traffic_zone": false,
	"in_odd_even_zone": false,
	"city": "تهران",
	"state": "تهران"
}

در این پروژه می‌خواهیم آدرس نقطه‌ای که بر روی نقشه با لمس طولانی مشخص می‌کنیم را از وب‌سرویس «دریافت آدرس از روی موقعیت» به دست آوریم و این آدرس را در یک Bottom sheet نمایش دهیم.



  • reverse_bottom_sheet.xml :

این فایل رابط کاربری Bottom sheet ای است که بر روی آن محله و آدرس یک نقطه نمایش داده خواهد شد.

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginStart="8dp"
    android:layout_marginEnd="8dp"
    android:clickable="true"
    android:focusable="true"
    app:cardElevation="8dp"
    app:cardCornerRadius="8dp"
    app:behavior_hideable="false"
    app:behavior_peekHeight="0dp"
    app:layout_behavior="android.support.design.widget.BottomSheetBehavior"
    android:fitsSystemWindows="true"
    android:layoutDirection="ltr">

    <android.support.constraint.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="16dp">

        <TextView
            android:id="@+id/title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="محله"
            android:textSize="16sp"
            android:fontFamily="@font/iran_sans_bold"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintEnd_toEndOf="parent"/>

        <TextView
            android:id="@+id/details"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:text="آدرس"
            android:textSize="14sp"
            android:fontFamily="@font/iran_sans"
            android:textDirection="rtl"
            app:layout_constraintTop_toBottomOf="@id/title"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"/>


    </android.support.constraint.ConstraintLayout>

</android.support.v7.widget.CardView>


  • activity_api_retrofit.xml:

در این فایل که رابط‌ کاربری کلی این بخش از پروژه است، علاوه بر المان نقشه نشان، Bottom sheetای که در فایل قبلی تعریف شده بود نیز include شده است.

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout 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=".activity.APIRetrofit">

    <org.neshan.ui.MapView
        android:id="@+id/map"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />


    <!--reverse bottom sheet-->
    <include
        android:id="@+id/reverse_bottom_sheet_include"
        layout="@layout/reverse_bottom_sheet" />


</android.support.design.widget.CoordinatorLayout>


  • build.gradle (Module: app):

برای استفاده از کتابخانه Retrofit دو خط زیر را به لیست وابستگی‌های پروژه در این فایل اضافه می‌کنیم.

    implementation 'com.squareup.retrofit2:retrofit:2.3.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.3.0'


  • AndroidManifest.xml :

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

 <uses-permission android:name="android.permission.INTERNET" />


  • NeshanAddress.java :

در این مرحله، مدل داده پاسخ سرور را می‌سازیم. دو نوع پاسخ ممکن است از سمت سرور دریافت شود، یک نمونه آن - زمانی که وب‌سرویس به درستی می‌تواند آدرس موقعیت جغرافیایی مورد نظر را به دست آورد. حالت دیگری نیز وجود دارد که وب‌سرویس نمی‌تواند آدرس مناسبی برای محل مورد نظر ما پیدا کند. فرض کنید درخواست ما درخواست زیر باشد:

در این حالت پاسخ سرور به صورت زیر است:

{
	"status": "ERROR",
	"code": 404,
	"message": "Requested location not found."
}

برای این که هر دو حالت پاسخ‌های سرور را بتوانیم مدیریت کنیم، مدلی میسازیم که تمامی متغیرهای مربوط به هر دو پاسخ را داشته باشد.

در این کلاس، ابتدا تمامی متغیرهای مربوط به پاسخ در هر دو حالت تعریف شده است و سپس برای هر حالت یک متد سازنده تعریف شده است. تمامی متدهای setter و getter نیز تعریف شده اند.

package org.neshan.sample.starter.model;

import com.google.gson.annotations.SerializedName;

public class NeshanAddress {

    // when address is found
    @SerializedName("neighbourhood")
    private String neighbourhood;
    @SerializedName("address")
    private String address;
    @SerializedName("municipality_zone")
    private String municipality_zone;
    @SerializedName("in_traffic_zone")
    private Boolean in_traffic_zone;
    @SerializedName("in_odd_even_zone")
    private Boolean in_odd_even_zone;
    @SerializedName("city")
    private String city;
    @SerializedName("state")
    private String state;

    // when address is not found
    @SerializedName("status")
    private String status;
    @SerializedName("code")
    private Integer code;
    @SerializedName("message")
    private String message;


    public NeshanAddress(String neighbourhood, String address, String municipality_zone, Boolean in_traffic_zone, Boolean in_odd_even_zone, String city, String state) {
        this.neighbourhood = neighbourhood;
        this.address = address;
        this.municipality_zone = municipality_zone;
        this.in_traffic_zone = in_traffic_zone;
        this.in_odd_even_zone = in_odd_even_zone;
        this.city = city;
        this.state = state;
    }

    public NeshanAddress(String status, Integer code, String message) {
        this.status = status;
        this.code = code;
        this.message = message;
    }

    public String getNeighbourhood() {
        return neighbourhood;
    }

    public void setNeighbourhood(String neighbourhood) {
        this.neighbourhood = neighbourhood;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public String getMunicipality_zone() {
        return municipality_zone;
    }

    public void setMunicipality_zone(String municipality_zone) {
        this.municipality_zone = municipality_zone;
    }

    public Boolean getIn_traffic_zone() {
        return in_traffic_zone;
    }

    public void setIn_traffic_zone(Boolean in_traffic_zone) {
        this.in_traffic_zone = in_traffic_zone;
    }

    public Boolean getIn_odd_even_zone() {
        return in_odd_even_zone;
    }

    public void setIn_odd_even_zone(Boolean in_odd_even_zone) {
        this.in_odd_even_zone = in_odd_even_zone;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public String getState() {
        return state;
    }

    public void setState(String state) {
        this.state = state;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}


  • RetrofitClientInstance.java:

برای انجام درخواست‌های تحت شبکه به یک REST API با استفاده از Retrofit، با استفاده از کلاس Retrofit.Builder یک شی از می‌سازیم و BASE_URL - که آدرس پایه وب‌سرویس است را به آن می‌دهیم.

توضیحات مربوط به پین‌کردن کلید عمومی را در بخش Public Key Pinnig از این آموزش می‌توانید مطالعه کنید.

package org.neshan.sample.starter.network;

import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

public class RetrofitClientInstance {

    private static Retrofit retrofit;
    private static final String BASE_URL = "https://api.neshan.org/v1/";

    public static Retrofit getRetrofitInstance() {
        // creating a CertificatePinner object and adding public key of neshan.org to it
        CertificatePinner certPinner = new CertificatePinner.Builder()
                .add("*.neshan.org",
                        "sha256/Cyg7e5STKgZCwdABdPZlqO5lQWSE0KbWr624HoIUuUc=")
                .build();

        // adding the created certPinner to OkHttpClient
        OkHttpClient client = new OkHttpClient.Builder()
                .certificatePinner(certPinner)
                .build();

        if (retrofit == null) {
            retrofit = new retrofit2.Retrofit.Builder()
                    .baseUrl(BASE_URL)
                    .addConverterFactory(GsonConverterFactory.create())
                    .client(client)
                    .build();
        }
        return retrofit;
    }
}


  • GetDataService.java:

نقاط پایانی (درخواست‌های مختلف به وب‌سرویس‌های مختلف نشان) را در این interface تعریف می‌کنیم.

در تعریف هر نقطه پایانی (Endpoint)، در خط اول کلید API خود را در سرآمد درخواست قرار داده‌ایم. در خط دوم مشخص کرده‌ایم که درخواست از نوع GET است. در خط سوم، نام متد، مقدار بازگشتی آن - که از نوع مدل داده‌ای است که در مراحل قبل ساختیم - و ورودی متد - که آدرسی کامل درخواست ما است - را می‌دهیم.

package org.neshan.sample.starter.network;

import org.neshan.sample.starter.model.NeshanAddress;

import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Headers;
import retrofit2.http.Url;

public interface GetDataService {

    // TODO: replace "YOUR_API_KEY" with your api key
    @Headers("Api-Key: YOUR_API_KEY")
    @GET
    Call<NeshanAddress> getNeshanAddress(@Url String url);
}


  • APIRetrofit.java:

آدرس دریافت شده از یک موقعیت توسط وب سرویس، در متغیر neshanAddress ذخیره می‌شود.

reverseBottomSheetView و reverseBottomSheetBehavior جهت ارتباط با Bottom sheet هستند.

در ادامه نیز المان‌های رابط کاربری که در Bottom sheet وجود دارد تعریف شده است.

    // Result of reverseGeocoding
    private NeshanAddress neshanAddress;

    // a bottomsheet to show address on
    private View reverseBottomSheetView;
    private BottomSheetBehavior reverseBottomSheetBehavior;

    //ui elements in bottom sheet
    private TextView addressTitle;
    private TextView addressDetails;

هنگامی که بر روی نقشه لمس طولانی شود، بعد از صدا زدن متد addMarker - برای اضافه شدن یک نشانگر به موقعیت لمس شده - متد neshanReverseAPI صدا زده می‌شود. این متد آدرس موقعیت کلیک شده را دریافت کرده و در Bottom sheet نمایش می‌دهد. این متد در ادامه توضیح داده خواهد شد.

    // Initializing layout references (views, map and map events)
    private void initLayoutReferences() {
        // Initializing views
        initViews();
        // Initializing mapView element
        initMap();

        // when long clicked on map, a marker is added in clicked location
        // MapEventListener gets all events on map, including single tap, double tap, long press, etc
        // we should check event type by calling getClickType() on mapClickInfo (from ClickData class)
        map.setMapEventListener(new MapEventListener(){
            @Override
            public void onMapClicked(ClickData mapClickInfo){
                super.onMapClicked(mapClickInfo);
                if(mapClickInfo.getClickType() == ClickType.CLICK_TYPE_LONG) {
                    // by calling getClickPos(), we can get position of clicking (or tapping)
                    LngLat clickedLocation = mapClickInfo.getClickPos();
                    // addMarker adds a marker (pretty self explanatory :D) to the clicked location
                    addMarker(clickedLocation);

                    //calling NeshanReverseAPI to get address of a location and showing it on a bottom sheet
                    neshanReverseAPI(clickedLocation);
                }
            }
        });
    }

در اینجا، علاوه بر دریافت المان نقشه، المان‌های موجود در Bottom sheet نیز دریافت می‌شود.

در ادامه خود Bottom sheet دریافت شده و رفتار آن در متغیر reverseBottomSheetBehavior ذخیره می‌شود. حالت ابتدایی آن نیز STATE_EXPANDED تعریف شده است.

    // We use findViewByID for every element in our layout file here
    private void initViews(){
        map = findViewById(R.id.map);

        // UI elements in bottom sheet
        addressTitle = findViewById(R.id.title);
        addressDetails = findViewById(R.id.details);

        reverseBottomSheetView = findViewById(R.id.reverse_bottom_sheet_include);
        reverseBottomSheetBehavior = BottomSheetBehavior.from(reverseBottomSheetView);
        // bottomsheet is collapsed at first
        reverseBottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
    }

در اینجا ابتدا یک GetDataService ساخته می‌شود.

سپس آدرس درخواست را با استفاده از آرگومان ورودی این متد به دست آمده است.

می‌خواهیم برای مکان‌هایی که آدرس آن‌ها به درستی به دست نیامده است، lat و lon آن‌ها را تا ۶ رقم بعد از ممیز نمایش دهیم. در خط سوم رشته مربوطه برای این کار آماده شده است.

برای اجرای یک درخواست، متدی که برای آن درخواست در GetDataService نوشته شده است صدا زده شده است و خروجی آن در متغیر call ذخیره می‌شود. برای بررسی پاسخ دریافتی از سرور، بر روی این متغیر متد enqueue صدا زده می‌شود. یک Callback<NeshanAddress> جدید به عنوان ورودی به آن داده می‌شود و دو متد onResponse و onFailure در آن Override می‌شود.

در متد onResponse بررسی می‌شود که اگر کد پاسخ برابر با ۲۰۰ باشد (پاسخ درستی از سمت سرور دریافت شود)، اگر در بدنه پاسخ code وجود نداشته باشد، یعنی حالت اول پاسخ که آدرس به درستی به دست آمده است رخ داده است. در این حالت، اطلاعات محله و آدرس در Bottom sheet نمایش داده می‌شود. در صورتی که code در بدنه پیام وجود داشته باشد، حالتی رخ داده است که آدرس به درستی به دست نیامده است. در این حالت در Bottom sheet عبارت «آدرس نامشخص» و طول و عرض جغرافیایی آن نوشته می‌شود.

    private void neshanReverseAPI(LngLat loc) {
        GetDataService retrofitService = RetrofitClientInstance.getRetrofitInstance().create(GetDataService.class);
        String requestURL = "https://api.neshan.org/v1/reverse?lat=" + loc.getY() + "&lng=" + loc.getX();
        final String latLngAddr = String.format("%.6f", loc.getY()) + "," + String.format("%.6f", loc.getX());

        Call<NeshanAddress> call = retrofitService.getNeshanAddress(requestURL);
        call.enqueue(new Callback<NeshanAddress>() {
            @Override
            public void onResponse(Call<NeshanAddress> call, Response<NeshanAddress> response) {
                Log.d("neshanAddress", response.body().getAddress());
                if (response.code() == 200) {
                    if (response.body().getCode() == null) {
                        // if code is null in answer. By checking 2 kind of answers
                        neshanAddress = response.body();
                        addressTitle.setText(neshanAddress.getNeighbourhood());
                        addressDetails.setText(neshanAddress.getAddress());
                    } else {
                        addressTitle.setText("آدرس نامشخص");
                        addressDetails.setText(latLngAddr);
                    }
                }
            }

            @Override
            public void onFailure(Call<NeshanAddress> call, Throwable t) {
                Log.d("neshanAddress", "failure");
            }
        });
    }




صفحه قبل (لایه پایگاه داده)

صفحه بعد (Volley)