05 August 2015

ทำ Location Provider แบบเหนือชั้นด้วย Smart Location Library

Updated on


        Location Provider หรือที่ชอบเรียกกันว่า GPS เป็นอีกหนึ่งลูกเล่นที่แอปพลิเคชันหลายๆตัวนิยมใช้งานกัน แต่ทว่าถ้านักพัฒนาจัดการกับมันได้ไม่ดี มันก็จะกลายเป็นส่วนหนึ่งของการบริโภคพลังงาน ซึ่งเจ้าของบล็อกก็เคยแนะนำให้เปลี่ยนไปใช้ Google Location Services API แทน เพราะทั้งง่ายและมีประสิทธิภาพ

        แต่ทว่า Google Location Services API ก็มีข้อจำกัดตรงที่ตัวมันเองผูกกับ Google Play Services ภายในเครื่อง ถ้าเครื่องนั้นๆไม่มี Google Play Services ก็จะใช้งานไม่ได้ (แต่เชื่อเถอะ อุปกรณ์แอนดรอยด์ 90% บนโลกนี้มีหมดแหละ) ดังนั้นถ้าเครื่องที่ไม่มีก็จะต้องทำให้มันเรียกใช้โค๊ดธรรมดาๆได้ สุดท้ายก็จะกลับเข้าสู่ลูปเดิมคือเรื่องประสิทธิภาพ

        ดังนั้นวันนี้เจ้าของบล็อกจึงมาขอแนะนำอีก Solution เพื่อช่วยให้นักพัฒนามีชีวิตสะดวกสบายยิ่งกว่าเก่า ด้วย Library สำหรับ Location Provider ที่ชื่อว่า Smart Location

        Smart Location เป็นอีกหนึ่ง Library ที่น่าสนใจไม่น้อยสำหรับเรียกใช้งาน Location Provider เพราะมันผนวกตัวเองให้มีทั้งการทำงานของ Google Play Service และโค๊ดแบบธรรมดาๆ โดยที่นักพัฒนาแค่นำไปเรียกใช้งานได้เลย ไม่ต้องมานั่งเขียนเอง

คุณสมบัติที่น่าสนใจ

        • เป็นการรวมตัวของ Location Provider แบบธรรมดาและ Google Play Services
        • มี Geocoding ในตัว
        • มี Geofencing ด้วย ถ้าต้องการใช้งาน
        • มี Activity เพื่อรับรู้ได้ว่าผู้ใช้กำลังทำกิจกรรมอะไร เดิน วิ่ง หรือขี่จักรยาน
        • เรียกใช้งานแบบ Fluent interface
        • เรียกใช้งานง่ายมากถึงมากที่สุด

เริ่มต้นใช้งาน

        เพิ่ม Dependencies เข้าไปใน build.gradle ดังนี้

compile 'io.nlopez.smartlocation:library:3.2.0'

        เนื่องจาก Library ตัวนี้มีการใช้งาน Google Play Services ด้วย จึงมี Dependencies ของ Google Play Services อยู่ในนี้ด้วย และถ้าโปรเจคของผู้ที่หลงเข้ามาอ่านมี Dependencies ของ Google Play Services เหมือนกันก็อาจจะทำให้มันซ้ำซ้อนกันได้ ดังนั้นให้แก้ไขด้วยการประกาศ Dependencies ของ Library แบบนี้แทน

compile ('io.nlopez.smartlocation:library:3.0.0') {
    transitive = false
}

        เพิ่มเติม หมั่นเช็คเวอร์ชันของ Library จาก GitHub อยู่บ่อยๆ เพราะเลขเวอร์ชันในบทความนี้อาจจะไม่ใช่เวอร์ชันล่าสุดเสมอไป

        เพิ่ม Meta Data ของ Google Play Services ไว้ใน Android Manifest ด้วย ให้ใส่ไว้ใน <application> นะ

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

    <application
        ...
        ... >
        
        ...

        <meta-data
            android:name="com.google.android.gms.version"
            android:value="@integer/google_play_services_version"
            tools:replace="android:value" />
    </application>

</manifest>

        จะเห็นว่ามีคำสั่ง tools:replace ด้วย เพราะว่ามีการประกาศซ้ำซ้อนกับใน Library จึงต้องใช้คำสั่งนี้เพื่อให้แทนที่ของเดิมใน Library โดยจะต้องประกาศ Namespace ให้กับ tools ด้วย เพราะปกติใน Android Manifest จะไม่ได้ใช้งาน Namespace ตัวนี้ซักเท่าไร (ถ้ามีแล้วก็ไม่ต้องประกาศ)

        สำหรับ Permission ไม่ต้องกำหนดเพิ่มเติมก็ได้ เพราะว่าใน Library กำหนดไว้แล้ว เดี๋ยวตอน Build Gradle มันก็จะจัดการกับ Permission ให้เองโดยอัตโนมัติ

จะเรียกใช้งานยังไง?

        ก่อนจะดูว่ามีคำสั่งอะไรบ้าง ให้ลองดูตัวอย่างการใช้คำสั่งของ Smart Location ดูก่อน

import android.location.Location;
import android.os.Bundle;
import android.app.Activity;

import io.nlopez.smartlocation.OnLocationUpdatedListener;
import io.nlopez.smartlocation.SmartLocation;

public class MainActivity extends Activity implements OnLocationUpdatedListener {

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

    @Override
    protected void onResume() {
        super.onResume();
        SmartLocation.with(this)
                .location()
                .start(this);
    }

    @Override
    protected void onPause() {
        super.onPause();
        SmartLocation.with(this)
                .location()
                .stop();
    }

    @Override
    public void onLocationUpdated(Location location) {
        // Do something here when location was updated
    }
}

        นี่คือตัวอย่างคำสั่งอย่างง่ายในการเรียกใช้งาน Location Provider ของ Smart Location

        โคตร Smart!!

        ด้วยการที่ตัว Library ทำเป็น Static จึงทำให้สามารถเรียกใช้งานจากที่ไหนก็ได้โดยโยน Context ให้มัน แล้วประกาศ OnLocationUpdatedListener เพื่อรอรับค่าตำแหน่งของอุปกรณ์ เป็นอันเสร็จ!!

มีอะไรให้เรียกใช้งานบ้าง?

        Smart Location จะแบ่งความสามารถออกเป็น 4 แบบด้วยกัน ดังนี้

        • Location สำหรับหาตำแหน่งพิกัดของอุปกรณ์
        • Activity สำหรับคาดเดาพฤติกรรมการเคลื่อนที่ของผู้ใช้ เช่น เดิน หรือวิ่ง เป็นต้น
        • Geofencing สำหรับกำหนดขอบเขตสถานที่ เช่น ผู้ใช้เข้ามา/ออกนอกระยะบริเวณบ้าน เป็นต้น
        • Geocoding สำหรับค้นหาชื่อสถานที่จากพิกัดหรือค้นหาพิกัดของสถานที่ที่ต้องการ

การใช้งาน Location

        สมมติว่าผู้ที่หลงเข้ามาอ่านต้องการอ่านพิกัดของเครื่อง ณ ตอนนั้น จะใช้คำสั่งง่ายๆดังนี้

SmartLocation.with(context)
        .location()
        .start(onLocationUpdatedListener);

        ซึ่งคำสั่งนี้จะกำหนดค่า Deault เตรียมไว้ให้แล้ว ที่ต้องกำหนดคือ Context

        และเมื่อต้องการสั่งให้หยุดทำงาน

SmartLocation.with(context)
        .location()
        .stop();

        คำสั่ง start จะเรียกใช้งานตอนไหนก็ได้ ตามที่ต้องการ แต่ก็อย่าลืม stop ทุกครั้งเมื่อไม่ใช้งานด้วยนะ จะเอาไว้ใน onStop หรือ onDestroy ก็ได้ ขึ้นอยู่กับความเหมาะสม

        ง่ายดีเนอะ 

        แต่ยังไม่หมดนะ เพราะในความเป็นจริง ผู้ที่หลงเข้ามาอ่านมักจะต้องการกำหนดค่าบางอย่างเองด้วย สมมติว่าเจ้าของบล็อกต้องการค้นหาพิกัดเพียงแค่ครั้งเดียวเท่านั้น สามารถกำหนดแบบนี้ได้เลย

SmartLocation.with(context)
        .location()
        .oneFix()
        .start(onLocationUpdatedListener);

        หรือต้องการให้ค้นหาพิกัดเรื่อยๆจนกว่าจะสั่งหยุด

SmartLocation.with(context)
        .location()
        .continuous()
        .start(onLocationUpdatedListener);

        แต่ถ้าไม่กำหนดอะไรเลย ตัว Library จะกำหนดให้เป็นแบบต่อเนื่อง (Continuous) อยู่แล้ว

        ถ้าอยากกำหนดพวกความแม่นยำ ระยะทาง และระยะเวลาในการค้นหา ก็จะมี LocationParams ให้กำหนดค่า

LocationParams params = new LocationParams.Builder()
        .setAccuracy(accuracy)
        .setDistance(distance)
        .setInterval(interval)
        .build();
SmartLocation.with(context)
        .location()
        .config(params)
        .start(this);

        Accuracy จะกำหนดเป็น Enum ของคลาส LocationAccuracy ว่าอยากจะให้อ่านพิกัดแม่นยำมากน้อยแค่ไหน

LocationAccuracy.HIGH
LocationAccuracy.MEDIUM
LocationAccuracy.LOW

        Distance ระยะทางขั้นต่ำที่จะให้อัปเดตพิกัดใหม่อีกครั้ง กำหนดค่าเป็น Float โดยมีหน่วยเป็นเมตร
        Interval ระยะเวลาขั้นต่ำที่จะให้อัปเดตพิกัดใหม่อีกครั้ง กำหนดค่าเป็น Long โดยมีหน่วยเป็นมิลลิวินาที

        ตัวอย่างการกำหนดค่า

LocationParams params = new LocationParams.Builder()
        .setAccuracy(LocationAccuracy.HIGH)
        .setInterval(10000)
        .build();
SmartLocation.with(context)
        .location()​
        .config(params)
        .start(this);

        อ่านค่าแบบแม่นยำที่สุด โดยอัปเดตค่าพิกัดทุกๆ 10 วินาที

        เพิ่มเติม ไม่ต้องกำหนดให้ครบทั้ง 3 อันก็ได้ เดี๋ยว Library ก็จะดึงค่า Default มาใช้งานเอง

        และถ้าขี้เกียจกำหนดค่าหลายตัว ก็สามารถเรียกใช้งาน Enum จาก LocationParams ได้เลยนะ

LocationParams.BEST_EFFORT
LocationParams.NAVIGATION
LocationParams.LAZY

        ก็จะเรียงลำดับความแม่นยำ ความเร็วในการอ่านพิกัด จากมากไปน้อย ต้องการใช้งานแบบไหนก็ไปดูว่าแอปพลิเคชันของผู้ที่หลงเข้ามาอ่านเหมาะกับแบบไหน

SmartLocation.with(context)
        .location()​
        .config(LocationParams.NAVIGATION)
        .start(this);

        และคำสั่งที่เจ้าของบล็อกชอบมาก นั่นก็คือคำสั่งกำหนด Provider ที่จะเรียกใช้งาน โดย Provider ที่สามารถเรียกใช้งานได้จะมีดังนี้

        • LocationGooglePlayServicesProvider ใช้ Google Location Services API ในการอ่านค่าพิกัด
        • LocationManagerProvider ใช้คำสั่งจากคลาส LocationManager ที่ทาง Library เตรียมไว้ให้แล้ว
        • LocationBasedOnActivityProvider ใช้การเปลี่ยนแปลงของกิจกรรมที่ทำอยู่ มาเป็นตัวควบคุมการอ่านค่าพิกัด
        • LocationGooglePlayServicesWithFallbackProvider ใช้คำสั่งแบบผสมกันระหว่างคำสั่งจากคลาส LocationManager กับ Google Location Services API โดยให้ Google Location Services API เป็นตัวหลักเมื่ออุปกรณ์นั้นๆมี Google Play Services แต่ถ้าอุปกรณ์เครื่องนั้นไม่มีก็จะเปลี่ยนไปใช้คำสั่งจากคลาส LocationManager แทน

        เวลากำหนดก็จะกำหนดแบบนี้

SmartLocation.with(context)
        .location(new LocationGooglePlayServicesProvider(listener))
        .config(params)
        .start(context);
        ซึ่งแต่ละ Provider จะมี Listener อยู่ในตัวมันเองด้วย เพื่อส่งข้อมูลบางอย่างที่เจาะจงของ Provider แต่ละตัว อย่างเช่น LocationGooglePlayServiceProvier ก็จะ Listener เพื่อบอกสถานะการเชื่อมต่อกับ Google API Client หรือ LocationBasedOnActivityProvider ก็จะมี Listerner คอยบอกว่า Event ที่เกิดขึ้นเป็น Activity (กิจกรรม) แบบไหน

        ยกเว้น LocationGooglePlayServicesWithFallbackProvider เท่านั้นที่จะไม่มี Listener แต่ต้องโยน Context เข้าไปให้แทน

SmartLocation.with(context)
        .location(new LocationGooglePlayServicesWithFallbackProvider(context))
        .config(params)
        .start(context);
         ถ้าจะให้แนะนำก็คงจะเป็น LocationGooglePlayServicesWithFallbackProvider แต่เจ้าของบล็อกไม่รู้ว่า Default กำหนดเป็นอะไรไว้นะ และคำสั่งต่างๆที่อธิบายไปก็สามารถผสมผสานคำสั่งได้ตามต้องการเลย

        และถ้าอยากจะอ่านค่าพิกัดสุดท้ายที่เคยอ่านได้ก็จะมีคำสั่ง

Location location = SmartLocation.with(context)
        .location()
        .getLastLocation();

        โดยค่าที่ได้จะเป็นคลาส Location ไปดึงเอาข้อมูลที่ต้องการในนั้นได้เลย

        และที่สำคัญ อย่าลืมตรวจสอบก่อนนะว่า Location Services สามารถเรียกใช้งานได้หรือป่าว โดยจะมีคำสั่ง state ให้ใช้งานเพื่อตรวจสอบ ดังนี้

SmartLocation.with(context).location().state().locationServicesEnabled();
SmartLocation.with(context).location().state().isAnyProviderAvailable();
SmartLocation.with(context).location().state().isGpsAvailable();
SmartLocation.with(context).location().state().isNetworkAvailable();
SmartLocation.with(context).location().state().isPassiveAvailable();

        • locationServicesEnabled Location Services เปิดให้ใช้งานมั้ย?
        • isAnyProviderAvailable มี Location Provider ให้ใช้งานมั้ย (ตัวไหนก็ได้)?
        • isGpsAvailable GPS Provider สามารถใช้งานได้หรือไม่?
        • isNetworkAvailable Network Provider สามารถใช้งานได้หรือไม่?
        • isPassiveAvailable Passive Provider สามารถใช้งานได้หรือไม่?

        ดังนั้นการใช้งานที่ดีก็ควรตรวจสอบด้วยว่าผู้ใช้ปิด Location ไว้หรือไม่ ก่อนที่จะเรียกใช้คำสั่งของ Smart Location

import android.location.Location;
import android.os.Bundle;
import android.app.Activity
import android.util.Log;

import io.nlopez.smartlocation.OnLocationUpdatedListener;
import io.nlopez.smartlocation.SmartLocation;
import io.nlopez.smartlocation.location.providers.LocationGooglePlayServicesWithFallbackProvider;

public class MainActivity extends AppCompatActivity implements OnLocationUpdatedListener {

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

    @Override
    protected void onStart() {
        super.onStart();
        if(SmartLocation.with(this).location().state().locationServicesEnabled()) {
            SmartLocation.with(this)
                    .location()
                    .config(LocationParams.BEST_EFFORT)
                    .provider(new LocationGooglePlayServicesWithFallbackProvider(this))
                    .start(this);
        } else {
            locationServiceUnavailabled();
        }
    }

    @Override
    protected void onStop() {
        super.onStop();
        SmartLocation.with(this)
                .location()
                .stop();
    }

    @Override
    public void onLocationUpdated(Location location) {
        // TODO Do something when location was updated
        double latitude = location.getLatitude();
        double longitude = location.getLongitude();
        float accuracy = location.getAccuracy();
        float bearing = location.getBearing();
        String provider = location.getProvider();
    }

    private void locationServiceUnavailable() {
        // TODO Do something when location service is unavailable
    }
}

        ข้อดีของ Library ตัวนี้คือไม่ต้องกังวลว่าคำสั่งจะถูกเรียกซ้ำเรื่อยๆ หรือสั่ง stop ทั้งๆที่ยังไม่ start เพราะมันมีการตรวจสอบอยู่ข้างในคำสั่งอยู่แล้ว ดังนั้นไม่ต้องกังวลว่าจะเจอกับ Exception เว้นแต่ว่าผู้ที่หลงเข้ามาอ่านไปกำหนดค่าแบบผิดๆเอง

        ส่วนผู้ที่หลงเข้ามาอ่านจะใช้งานแบบไหนก็ดูความเหมาะสมด้วยนะครับ เพราะ Location Services ไม่จำเป็นต้องการความแม่นยำสูงเสมอไป แต่ขึ้นอยู่กับงานที่จะใช้เป็นหลัก

        และสำหรับ Geocoding, Geofencing และ Acitvity เจ้าของบล็อกไม่ขออธิบายนะ เพราะอยากอธิบายแค่ในส่วนของ Location เท่านั้น แต่ถ้าอยากศึกษาเพิ่มเติมก็สามารถเข้าไปดูกันได้ที่ smart-location-lib [GitHub]