استفاده از وب سرویس جستجوی نشان در اندروید

توی این آموزش میخوام طریقه استفاده از سرویس جستجوی مکان محور پلتفرم نقشه نشان برای اپ اندرویدیتون رو توضیح بدم . با پیاده سازی این سرویس میتونین خیابونا و میدونا و مکانا و ازینجور چیزا رو توی برنامتون جستجو کنین. اگه با SDK اندروید نشان آشنا نیستین پیشنهاد می‌کنم اول مطلب معرفی SDK نقشه نشان برای Android رو بخونین. همینطور برای آشنایی با سرویس‌‌های متنوع نشان می‌تونین مطلب سرویس‌های نقشه نشان برای برنامه‌نویسان رو هم بخونین.

کار این سرویس اینجوریه که سه تا چیز از شما میگیره، جستجو میکنه، نتیجه جستجو رو برمی‌گردونه به شما. اون سه تا چیز ازین قرارن: عبارت مورد جستجو، طول جغرافیایی و عرض جغرافیایی.

چیزی که میخوام توی این چند دقیقه توضیح بدم اینه که چجوری این سه تا چیز رو بفرستین برای سرور و چجوری نتیجه ای که واستون برمیگردونه رو دریافت کنین، همین.


عبارت جستجو و طول و عرض جغرافیایی رو اینطوری با یه درخواست با متد GET به سرور نشان ارسال میکنین:

https://api.neshan.org/v1/searchterm=SEARCH_TERM&lat=LATITUDE&lng=LONGITUDE

که همونطوریکه مشخصه ، به جای SEARCH_TERM ، باید عبارت جستجو قرار بگیره و عرض و طول جغرافیایی هم به ترتیب به جای LATITUDE و LONGITUDE قرار میگیرن.

یه چیز دیگه ام لازمه همراه این درخواست به سرور ارسال بشه! اگه تا اینجا اومدین احتمالا توی پنل توسعه دهندگان نشان عضو شدین . توی پنلتون ایجاد کلید دسترسی رو انتخاب کنین و یه اسم واسه سرویستون بذارین تا به صورت رایگان یک کلید دسترسی (API key) دریافت کنین.

یه همچین چیزی: service.PnRV9ocd8zm9QYYlJUNLJoAihE3hfy34WUZ6jkbc

کلیدی که دریافت کردین رو باید توی هدر درخواستتون بفرستین.

حالا که اینا رو فرستادین، نشان بهتون نتیجه جستجو رو به صورت JSON تحویل میده، کاری که شما باید بکنین اینه تحویلش بگیرین بعدشم parse کنینش تا هر قسمت از اطلاعات نتیجه‌ای که بهتون رسیده رو خواستین توی برنامتون استفاده کنین.

برای گرفتن این JSON و parse کردنش باید بدونین نشان قراره چه داده‌ای رو بهتون بده. با جستجوی کلمه گراز، این JSON رو دریافت میکنین:

{
    "count": 2,
    "items": [
        {
            "title": "روستای گراز آباد",
            "address": "کرمانشاه",
            "category": "region",
            "region": "کرمانشاه",
            "type": "village",
            "location": {
                "x": 46.68479996599142,
                "y": 34.57959997982072,
                "z": "NaN"
             }
        },
        {
            "title": "روستای گرازآباد",
            "address": "چهارمحال و بختیاری",
            "category": "region",
            "region": "چهارمحال و بختیاری",
            "type": "village",
            "location": {
                "x": 50.75553939150253,
                "y": 31.512362518883325 ,
                "z": "NaN"
            }
        }
    ]
}


اگه میخواین بدونین هر کدوم ازین فیلدهای JSON دقیقا چی هستن، به صفحه سرویس جستجوی نشان سر بزنید، البته اسم خود فیلدها هم خودشون رو خوب معرفی میکنن.

خب پس الآن مشخص شد چی میدین و چی میگیرین. اگه با پروتکل HTTP و Request دادن و Response گرفتن و parse کردن JSON آشنا باشین الآن دیگه میدونین چجوری برنامتون رو بنویسین.

اگرم نمیدونین که توی ادامه داستان با این چیزا آشنا میشین . این کارها رو میخوام با کتابخونه شناخته شده Retrofit انجام بدم که همه این کارها رو به قشنگی واسمون انجام میده. اگه با Retrofit آشنا نیستین توی ادامه آشنا میشین.

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

خب شروع میکنیم، اول باید Retrofit رو به برنامتون اضافه کنین، پس وابستگی های زیر رو به dependencyهای فایل build.gradle ماژول برنامتون اضافه کنین:

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

خط اول، Retrofit رو به برنامتون اضافه میکنه، خط دوم Retrofit Gson Converter که از کتابخونه Gson استفاده میکنه رو به برنامتون اضافه میکنه. ما میخوایم اطلاعاتی که از سرور دریافت میکنیم رو وارد برناممون کنیم، یعنی JSON رو به اشیای جاوا تبدیل کنیم. به این کار در اصطلاح میگن deserialization. به تبدیل اشیای جاوا به JSON میگن serialization.

این دوتا کار رو Gson برای ما انجام میده و ما درگیر کار با JSON نمیشیم. ما توی این برنامه فقط با deserialize کردن سر و کار داریم، چون فقط میخوایم JSON دریافتی از سرور رو به اشیای جاوا تبدیل کنیم و توی برناممون ازش استفاده کنیم.

خب

برای ارسال درخواست جستجو و دریافت نتیجه با Retrofit این 4 تا کار رو باید انجامشون بدیم:

  1. ساختن کلاس های مدل
  2. ایجاد نمونه ای از کلاس Retrofit
  3. ساختن اینترفیس برای مشخص کردن نحوه ارسال و دریافت اطلاعات
  4. استفاده از سه مورد بالا برای ارسال درخواست و دریافت پاسخ

اولین کاری که میکنیم اینه که یک بار دیگه به اطلاعات JSON دریافتی از سرور نگاه می کنیم:

{
    "count": 1,
    "items": [
        {
            "title": "حرم مطهر امام رضا (ع)",
            "address": "مشهد، خراسان رضوی",
            "neighbourhood": "حرم مطهر",
            "region": "مشهد، خراسان رضوی",
            "type": "religious",
            "category": "place",
            "location": {
                "x": 59.6157432,
                "y": 36.2880443
            }
        }
    ]
}

این یک مثال هست که تو صفحه وب سرویس جستجوی نشان وجود داره تا فرمت اطلاعات دریافتی از سرور رو نشون بده. بر اساس این اطلاعات، کلاس یا کلاس های معادلشون رو میسازیم:

یک JSON Object اصلی داریم که دوتا فیلد داره:

فیلد count که مقدارش یک عدد صحیح هست.

فیلد items که مقدارش یک JSON Array هست (به [ ] توجه کنید )

بنابراین اولین کلاسی که باید بسازیم اینجوریه: (NeshanSearch یک اسم دلخواه هست )

دقت کنید که نام فیلدها ( count و items ) همنام فیلدهای JSON باشه

public class NeshanSearch {
    private Integer count;
    private List<Item> items ;
    public Integer getCount() {
        return count;
    }
    public void setCount(Integer count) {
        this.count = count;
    }
    public List<Item> getItems() {
        return items;
    }
    public void setItems(List<Item> items) {
        this.items = items;
    }
}


یکی از فیلدهای کلاس NeshanSearch ، لیستی از کلاس Item هست که باید کلاس Item رو بسازیم، به JSON نگاه کنید، به ساختار Objectهایی که میتونن داخل items قرار بگیرند ( { } ) توجه کنید.

کلاس Item به این شکله:

public class Item {
    private String title;
    private String address;
    private String neighbourhood;
    private String region;
    private String type;
    private String category;
    private Location location;

    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
    public String getAddress() {
        return address;
    }
    public void setAddress(String address) {
        this.address = address;
    }
    public String getNeighbourhood() {
        return neighbourhood;
    }
    public void setNeighbourhood(String neighbourhood) {
        this.neighbourhood = neighbourhood;
    }
    public String getRegion() {
        return region;
    }
    public void setRegion(String region) {
        this.region = region;
    }
    public String getType() {
        return type;
    }
    public void setType(String type) {
        this.type = type;
    }
    public String getCategory() {
        return category;
    }
    public void setCategory(String category) {
        this.category = category;
    }
    public Location getLocation() {
        return location;
    }
    public void setLocation(Location location) {
        this.location = location;
    }
}

دقت کنید در کلاس Item، فیلد Location وجود داره که کلاس مربوط بهش باید ساخته بشه.

به آبجت location در JSON دقت کنید:

 "location": {
  "x": 59.6157432, 
  "y": 36.2880443
   } 

همونطور که میشه فهمید، کلاس Location باید به این صورت باشه:

public class Location {
    private Double x;
    private Double y;

    public Double getX() {
        return x;
    }
    public void setX(Double x) {
        this.x = x;
    }
    public Double getY() {
        return y;
    }
    public void setY(Double y) {
        this.y = y;
    }

پس خلاصه اینکه سه تا کلاس NeshanSearch و Item و Location به عنوان کلاس های معادل JSON دریافتی، ساخته شدن و در پکیج model.search گذاشتمشون.


خب حالا میخوام یک object از Retrofit داشته باشم و در طول برنامه از همین نمونه Retrofit استفاده میکنم. اصطلاحا یک کلاس Singleton میسازم که تو این کلاس یک Object از Retrofit تولید میشه.

public class RetrofitClientInstance{

    private static Retrofit retrofit;
    private static final String BASE_URL = "https://api.neshan.org/v1/";
    private RetrofitClientInstance(){
    }

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

علت اینکه کلاس RetrofitClientInstance رو Singleton میسازیم اینه که شی Retrofit به اصطلاح expensive هست و هزینه ساخت زیادی داره اگه برای هر درخواست یک نمونه جدید از Retrofit تولید بشه برنامه ممکنه با memory leak مواجه بشه و crash کنه.

با استفاده از متد baseUrl آدرس پایه سروری که قراره باهاش ارتباط برقرار کنیم رو تنظیم میکنیم و ادامه آدرس مورد نظرمون رو توی قسمت های بعدی میتونیم وارد کنیم.

با متد addConverterFactory نوع مبدل مورد نظرمون رو برای Retrofit تنظیم میکنیم. فرمت دریافت اطلاعات ما JSON هست و از Retrofit Gson Converter استفاده میکنیم.

نوبت قسمتی شده که میخوایم نحوه تعاملون با وب سرویس رو تعیین کنیم و مشخص کنیم چطوری میخوایم اطلاعات رو به سرور بفرستیم:

public interface GetDataService {
    @Headers("Api-Key: YOUR_API_KEY")
    @GET
    Call<NeshanSearch> getNeshanSearch(@Url String url);
}

توی این اینترفیس متدی رو که قراره به وسیله اون اطلاعات رو به سرور بفرستیم و دریافت کنیم، مینویسیم.

ما قرار نیست این متد رو پیاده سازی کنیم، Retrofit زحمتش رو برامون میکشه، ولی به وسیله annotation هایی که Retrofit در اختیارمون میذاره باید مشخص کنیم که چه چیزی رو قراره چطور بفرستیم.

با GET@ میگیم نوع درخواستمون Get هستش. با Headers@، همونطور که در ابتدای این مطلب گفتم، api_key رو به عنوان هدر میفرستیم. چیزی که قرار شد به سرور بفرستیم یک url ساده هست که داخلش عبارت جستجو و طول و عرض جغرافیایی قرار داره، با Url@ میگیم که این آرگومان ورودی یک url هست.

اون چیزی هم که این متد برمیگردونه یک Call<NeshanSearch> هست، یعنی همون کلاس اصلی مدلی که ساختیم.

این اینترفیس و کلاس RetrofitClientInstance رو میتونین توی پکیج network برنامه پیدا کنین.

خب دیگه داریم به انتهای راه نزدیک میشیم. الآن وقتشه ازین چیزایی که ساختیم استفاده کنیم.

توی اکتیویتی مربوط به جستجو، یک متد به اسم doSearch مینویسیم و کارهای مربوط به جستجو رو انجام میدیم:

private List<Item> items;
private void doSearch(String term) {
    final double lat = map.getFocalPointPosition().getY();
    final double lng = map.getFocalPointPosition().getX();
    final String requestURL = "https://api.neshan.org/v1/search?term=" + term + "&lat=" + lat + "&lng=" + lng;

    GetDataService api = RetrofitClientInstance.getRetrofitInstance().create(GetDataService.class);
    Call<NeshanSearch> call = api.getNeshanSearch(requestURL);

    call.enqueue(new Callback<NeshanSearch>() {
        @Override
        public void onResponse(Call<NeshanSearch> call, Response<NeshanSearch> response) {
            if (response.isSuccessful()) {
                NeshanSearch neshanSearch = response.body();
                items = neshanSearch.getItems();
                adapter.updateList(items);
            } else {
                Log.i(TAG, "onResponse: " + response.code() + " " + response.message());
                Toast.makeText(Search.this, "خطا در برقراری ارتباط!", Toast.LENGTH_SHORT).show();
            }
        }

        @Override
        public void onFailure(Call<NeshanSearch> call, Throwable t) {
            Log.i(TAG, "onFailure: " + t.getMessage());
            Toast.makeText(Search.this, "ارتباط برقرار نشد!", Toast.LENGTH_SHORT).show();
        }
    });

}

نو این متد، عبارتی که قراره جستجو بشه به عنوان آرگومان ورودی دریافت میشه. حالا باید url که در ابتدای مطلب گفته شد، تهیه بشه. عبارت مورد جستجو که از بیرون متد ارسال میشه، میمونه طول و عرض جغرافیایی مرجع که نشان باید جستجو رو حول اون نقطه انجام بده.

 double lng = map.getFocalPointPosition().getX(); 
 double lat = map.getFocalPointPosition().getY();  

با این دو خط، طول و عرض جغرافیایی وسط نقشه گرفته میشه. ( map یک نمونه از کلاس MapView نشان است که اگر با آن آشنا نیستید این مطلب رو بخونید )

 GetDataService api = RetrofitClientInstance.getRetrofitInstance().create(GetDataService.class);   
   Call<NeshanSearch> call = api.getNeshanSearch(requestURL); 

با این دو خط، به وسیله کلاس RetrofitClientInstance و اینترفیس GetDataService و متد getNeshanSearch که از قبل آمادشون کردیم، آماده ارسال جستجو و دریافت نتیجه میشیم.

و اینم حرکت آخر و اصلی

call.enqueue(new Callback<NeshanSearch>() {
    @Override
    public void onResponse(Call<NeshanSearch> call, Response<NeshanSearch> response) {
        if (response.isSuccessful()) {
            NeshanSearch neshanSearch = response.body();
            items = neshanSearch.getItems();
            adapter.updateList(items);
        } else {
            Log.i(TAG, "onResponse: " + response.code() + " " + response.message());
            Toast.makeText(Search.this, "خطا در برقراری ارتباط!", Toast.LENGTH_SHORT).show();
        }
    }

    @Override
    public void onFailure(Call<NeshanSearch> call, Throwable t) {
        Log.i(TAG, "onFailure: " + t.getMessage());
        Toast.makeText(Search.this, "ارتباط برقرار نشد!", Toast.LENGTH_SHORT).show();
    }
});

با استفاده از متد enqueue اطلاعاتی که باید بفرستیم (url و header) رو میفرستیم و اطلاعاتی که باید دریافت کنیم (کلاس NeshanSearch) رو در متد onResponse و در صورت موفقیت آمیز بودن دریافت اطلاعات ( if (response.isSuccessful())) ، دریافت میکنیم.

 NeshanSearch neshanSearch = response.body();

به این صورت به کلاس NeshanSearch که ساختیم و اطلاعات JSON که درون فیلدهای این کلاس قرار گرفته دسترسی داریم و میتونیم نام مکان های یافت شده و آدرس و طول و عرض جغرافیایی آن مکان ها و همه اطلاعات موجود در JSON را در قالب کلاس های جاوا دریافت کنیم.

سوررس کد کامل این آموزش در ریپوی اپلیکیشن استارتر نشان وجود داره. می‌تونین با مراجعه به کد یا ویکی این پروژه اطلاعات بیشتری کسب کنین.