ทุกวันนี้ผู้ที่หลงเข้ามาอ่านยังเขียน Location Provider เองอยู่หรือป่าว? เจ้าของบล็อกเชื่อว่านักพัฒนาในบ้านเรามากกว่า 50% ยังคงทำแบบนั้นอยู่ แต่รู้หรือไม่? ว่าตอนนี้ในเว็ป Android Developer ได้แนะนำว่าให้เลิกใช้วิธีแบบนั้นได้แล้ว และเปลี่ยนไปใช้ Google Location Services API แทน
บทความนี้เจ้าของบล็อกจะมาแนะนำให้รู้จัก Google Location Services API เพื่อให้ผู้ที่หลงเข้ามาอ่านเลิกใช้วิธีแบบเดิมๆ และเปลี่ยนมาใช้วิธีที่ง่ายกว่าและมีประสิทธิภาพดีกว่าที่ทาง Google แนะนำนะครับ
พื้นฐานคร่าวๆเกี่ยวกับ Location Provider
ถึงแม้ว่าจะแนะนำให้ใช้ Google Location Services API ไปเลย แต่ก็ควรรู้พื้นฐานของ Location Provider บนแอนดรอยด์กันด้วย จึงขออธิบายคร่าวๆให้ได้อ่านกันก่อนจะไปเริ่มใช้งาน Google Location Services API
Location Provider ที่ใช้กันในแอนดรอยด์จะมีอยู่ด้วยกัน 2 แบบคือ GPS Provider และ Network Provider
GPS Provider เป็นการใช้ GPS Module ที่อยู่ในอุปกรณ์แอนดรอยด์ โดย GPS จะอ้างอิงตำแหน่งด้วยดาวเทียมที่โคจรอยู่รอบๆโลก ซึ่งมีข้อดีคือมีความแม่นยำสูง (ถ้า GPS เครื่องไหนดีๆก็คลาดเคลื่อนไม่เกิน 10 เมตร) แต่ข้อเสียก็คือใช้เวลาในการค้นหาตำแหน่งค่อนข้างนาน ไม่สามารถใช้ภายในอาคารหรือที่อับสัญญาณได้ จะคลาดเคลื่อนได้ง่ายเพราะต้องรับสัญญาณจากดาวเทียม และใช้พลังงานเยอะ
Network Provider เป็นการใช้สัญญาณจาก Cellular หรือ WiFi ในการอ้างอิงตำแหน่ง เพราะเสาสัญญาณแต่ละตัวจะติดตั้งไว้ที่ตำแหน่งตายตัวและมีขอบเขตจำกัด จึงทำให้ระบุได้คร่าวๆว่าอยู่ที่บริเวณไหน ซึ่งมีข้อดีคือจับตำแหน่งได้ไวเพราะสื่อสารกับเสาสัญญาณ ณ จุดนั้นๆ แต่ข้อเสียคือความแม่นยำต่ำ มีความคลาดเคลื่อนสูง (100 เมตรขึ้นไป)
จะเห็นว่า Provider ทั้ง 2 แบบนั้นมีข้อเสียและข้อดีที่แตกต่างกัน ทั้งนี้ขึ้นอยู่กับงานว่าต้องการใช้งานแบบไหน แต่ก็สามารถใช้งานร่วมกันได้ ซึ่งจะเรียกว่า Fused Provider (เดี๋ยวอธิบายให้ทีหลัง)
โดยในช่วงแรกๆที่ GPS Provider ยังระบุตำแหน่งไม่ได้ ก็จะเป็นการระบุตำแหน่งด้วย Network Provider แทนไปก่อน และเมื่อ GPS Provider จับตำแหน่งได้แม่นยำแล้วก็จะใช้ตำแหน่งจาก GPS Provider แทน
ซึ่งวิธีนี้มีประสิทธิภาพมาก ติดแค่อย่างเดียว
"เปลืองแบตมาก"
Location Provider เป็นการทำงานอีกอย่างหนึ่งที่ "กินแบต" และจะ "กินแบตมากขึ้น" ถ้าจัดการมันไม่ดีพอ ลองถามตัวเองดูว่าแอปพลิเคชันของคุณมีการใช้งาน Location Provider อยู่หรือไม่ และถ้าใช้อยู่ คิดว่าโค๊ดที่ใช้ในตอนนี้มันจัดการกับ Location Provider ได้ดีพอแล้วหรือป่าว?
ดีพอหรือป่าวไม่รู้ เพราะทำตามในอินเตอร์เน็ต แค่มันใช้งานได้ก็พอแล้วนี่?
นี่คือสาเหตุหลักที่ทำให้การใช้งาน Location Provider ไม่มีประสิทธิภาพ เพราะข้อมูลในอินเตอร์เน็ตแค่บอกว่าทำตามแล้วใช้งานได้ แต่ไม่ได้บอกว่ามันจะเป็น Best Practice ในการใช้งาน Location Provider
โค๊ดของผู้ที่หลงเข้ามาอ่านแค่สั่งให้มันทำงาน แล้วไม่ได้จัดการอะไรต่อ และมันยังคงทำงานอยู่ถึงแม้จะปิดแอปพลิเคชันไปแล้วใช่มั้ย?
จึงทำให้ทีมแอนดรอยด์เข็น API สำหรับเรียกใช้งาน Location Provider ออกมาให้นักพัฒนาได้ใช้งานกัน โดยยัดมันลงไปใน Google Play Services โดยมีชื่อเรียกว่า Google Location Services API
แต่ถ้าจะให้ทุกๆตัวเรียกใช้งาน Location Provider แยกกันก็คงทำให้สูบแบตไม่น้อย ดังนั้นเค้าจึงรวมเป็น API ไว้ใน Google Play Services ซะเลย เพื่อที่ว่าจะได้เรียกใช้งานจากที่เดียวเลย รวมไปถึงเปิดให้นักพัฒนาแอปฯต่างๆเข้าใช้งานได้ด้วย
ซึ่ง Google Location Services API มีข้อดีก็ตรงที่นักพัฒนาไม่ต้องจัดการเรื่อง Battery/Performance Optimizing เลย เพราะ API ตัวนี้จัดการให้หมดแล้ว
และ Permission ที่ต้องประกาศมีเพียงแค่ 2 ตัว ที่ไม่จำเป็นต้องประกาศทั้ง 2 ตัว
ประกาศแค่ตัวใดตัวหนึ่ง?
ใช่ครับ ขึ้นอยู่กับว่าต้องการความแม่นยำของตำแหน่งผู้ใช้งานมากน้อยแค่ไหน ซึ่งเดี๋ยวเจ้าของบล็อกจะอธิบายให้ฟังทีหลังนะ ตอนนี้ประกาศทั้ง 2 ตัวไปก่อนก็ได้
เพิ่มเติม ที่ต้องทำเพิ่มก็คือ Runtime Permission ซึ่งเป็นฟีเจอร์ของ Android 6.0 Marshmallow ที่นักพัฒนาต้องเขียนเพิ่มเพื่อขออนุญาตผู้ใช้เพื่อเข้าถึง Location Service ในตัวเครื่องได้ Google Play Services and Runtime Permissions [Google APIs for Android]
นี่คือคำสั่งสำหรับการเชื่อมต่อ Google API Client เพื่อใช้งาน Google Location Services API นะครับ ถ้าจะเชื่อมต่อกับบริการอย่างอื่นด้วยก็อาจจะต้องมีคำสั่งอื่นเพิ่มเข้ามาด้วย
เมื่อเอาไปใช้งานจริงๆก็จะเป็นแบบนี้ (ยาวหน่อยนะ)
ในการเชื่อมต่อกับ Google API Client จะต้องมีการกำหนดค่าต่างๆก่อน เช่น
addApi(...) กำหนดว่าจะเชื่อมต่อกับ API ตัวไหนของ Google Play Services
addConnectionCallbacks(...) กำหนด Callback สำหรับสถานะการเชื่อมต่อกับ Google API Client
addConnectionFailedListener(...) กำหนด Listener เมื่อเชื่อมต่อกับ Google API Client ไม่สำเร็จ
แล้วจบท้ายด้วยคำสั่ง build() เพื่อสร้าง Instance ของ Google API Client ขึ้นมา และเริ่มเชื่อมต่อด้วยคำสั่ง connect()
ส่วน Callback และ Listener เจ้าของบล็อกประกาศ Implement ไว้ที่ MainActivity ไปเลย ซึ่งจะได้ Method มาทั้งหมด 3 ตัวด้วยกัน
onConnected(...) เป็น Method ของ ConnectionCallback ทำงานเมื่อเชื่อมต่อกับ Google API Client ได้สำเร็จ
onConnectionSuspended(...) เป็น Method ของ ConnectionCallback ทำงานเมื่อการเชื่อมต่อกับ Google API Client ถูกยกเลิกกลางคัน
onConnectionFailed(...) เป็น Method ของ OnConnectionFailedListener ทำงานเมื่อไม่สามารถเชื่อมต่อกับ Google API Client
รวมไปถึง Handle การเชื่อมต่อกับ Google API Client ในเวลาที่แอปพลิเคชันเกิด onStart() และ onStop() ด้วย โดยย้ายคำสั่ง connect() ไปไว้ใน onStart() เพื่อให้เชื่อมต่อทุกครั้งเวลาที่กลับเข้ามาใช้งานแอปพลิเคชัน และใช้คำสั่ง disconnect() เพื่อยกเลิกการเชื่อมต่อทุกครั้งใน onStop() หรือ onDestroy() (ออกจากแอปพลิเคชัน)
ก็แหงสิ ต้องเรียกใช้งาน Location Services ใน onConnected(...) แล้วสิ ส่วน Method อีกสองตัวที่เหลือก็มีไว้ Handle เมื่อ Google API Client ใช้งานไม่ได้ (ตรงนี้เจ้าของบล็อกจะไม่พูดถึง)
ก่อนจะใช้งาน Location Services ทุกครั้งให้ตรวจสอบก่อนว่า Location Provider บนอุปกรณ์แอนดรอยด์เครื่องน้ันๆเปิดอยู่หรือไม่ เพราะผู้ใช้หลายๆคนชอบปิดเพื่อประหยัดแบตกัน
ถ้าพร้อมใช้งานแล้วก็ให้ใช้คำสั่ง Location Services ได้เลย แต่ถ้าไม่ได้เปิดให้ใช้งานก็ให้แจ้งผู้ใช้งานซะว่า Location Provider ใช้ไม่ได้นะ หรือจะทำให้กดเพื่อไปหน้าเปิดใช้งาน Location Provider ก็ได้นะ เดี๋ยวพอผู้ใช้กดเปิดแล้วกลับมาเข้าแอปพลิเคชัน onStart() ก็จะทำงานอีกครั้งและ Google API Client ก็จะเชื่อมต่อใหม่โดยอัตโนมัติเอง
เมื่อ Location Provider พร้อมใช้งานแล้วก็เรียก Location Services ด้วยคำสั่งประมาณนี้
สิ่งแรกที่ควรมีเลยคือ Location Request เพื่อตั้งค่าสำหรับ Location Provider เช่น ความแม่นยำ ระยะเวลาในการทำงาน อ่านพิกัดได้กี่ครั้งแล้วหยุดทำงาน เป็นต้น
setExpirationDuration(millis: Long) กำหนดว่าจะให้อ่านพิกัดเป็นระยะเวลานานเท่าไร (หน่วยเป็นมิลลิวินาที)
setExpirationTime(millis: Long) กำหนดว่าจะให้อ่านพิกัดจนถึงเมื่อไร โดยนับเวลาตั้งแต่เปิดเครื่องขึ้นมา (หน่วยเป็นมิลลิวินาที)
setFastestInterval(millis: Long) กำหนดระยะเวลาในการอ่านพิกัดแต่ละครั้งที่เร็วที่สุดเท่าที่ทำได้ (หน่วยเป็นมิลลิวินาที)
setInterval(millis: Long) กำหนดระยะเวลาในการอ่านพิกัดแต่ละครั้ง (หน่วยเป็นมิลลิวินาที)
setNumUpdates(numUpdates: Int) จำนวนครั้งในการอ่านพิกัด
setPriorty(priority: Int) กำหนดความสำคัญในการอ่านข้อมูล ซึ่งมีผลไปถึงการใช้แบตเตอรีของเครื่องด้วย
setSmallestDisplacement(smallestDisplacementMeters: Float) ระยะทางขั้นต่ำที่จะให้อ่านพิกัดใหม่อีกครั้ง
Location Request จะช่วยให้กำหนดขอบเขตการทำงานของ Location Provider ได้ เพราะในบางครั้งแอปพลิเคชันอาจจะไม่จำเป็นต้องรู้พิกัดแม่นยำมากก็ได้ เช่น แค่อยากรู้ว่าอยู่เขตไหนในกรุงเทพ เป็นต้น แต่คำสั่งสำคัญที่อยากให้ดูกันก็คือ setPriority(...) ที่เอาไว้กำหนดความแม่นยำในการทำงาน
• LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY เน้นการอ่านพิกัดโดยใช้พลังงานที่เหมาะสม แต่ก็ได้ความแม่นยำพอสมควร โดยพิกัดที่ได้จะมีความแม่นยำในระยะ 100 เมตร (รู้ว่าอยู่ถนนอะไร)
• LocationRequest.PRIORITY_HIGH_ACCURACY เน้นการอ่านพิกัดโดยให้ได้ความแม่นยำสูงสุดเท่าที่ทำได้ ซึ่งจะเปลืองแบตมากที่สุด (รู้ว่ายืนอยู่ตรงไหน)
• LocationRequest.PRIORITY_LOW_POWER เน้นการอ่านพิกัดโดยใช้พลังงานน้อยที่สุด ความแม่นยำจะต่ำมาก คลาดเคลื่อนได้มากถึง 10 กิโลเมตร (อยู่ว่าอยู่เขตไหน ตำบลไหน)
• LocationRequest.PRIORITY_NO_POWER เป็นการอ่านพิกัดแบบ Passive คือไม่มีการสั่งให้ Google Play Services อ่านพิกัด แต่จะรอจนว่า Google Play Services จะอ่านพิกัดตามการทำงานปกติจึงค่อยนำข้อมูลนั้นมา Update ซึ่งการทำงานแบบนี้จะไม่ทำให้ใช้พลังงานเพิ่ม แต่ Interval ของข้อมูลจะนานมาก
ยกตัวอย่างการกำหนด Location Request
จากตัวอย่างนี้กำหนดว่าอ่านพิกัดแบบแม่นยำที่สุด โดยจะอ่านพิกัดทุกๆ 5 วินาที แต่ถ้าเครื่องสามารถจับพิกัดได้ก่อน ก็จะอ่านพิกัดทันทีทุกๆ 1 วินาที
และสมมติว่ากำหนดแค่ Priority ล่ะ?
ถ้ากำหนด Location Request แบบนี้จะทำให้ Location Provider อ่านพิกัดเพียงแค่ครั้งเดียวเท่านั้นแล้วก็หยุดทำงาน เพราะว่าไม่มีการกำหนด Interval
ดังนั้นควรถ้าต้องการให้อ่านพิกัดตลอดเวลาก็อย่าลืมกำหนด Interval ด้วยนะครับ
หลังจากกำหนด Location Request เสร็จเรียบร้อยแล้วก็จะเรียกใช้งาน Location Services ด้วยคำสั่ง
จะเห็นว่าต้องกำหนด Google API Client และ Location Request ซึ่งประกาศไว้เรียบร้อยแล้ว เอามากำหนดในคำสั่งนี้ได้เลย และในการทำงานของ Location Services จะมี Listener ที่ชื่อว่า Location Listner เพื่อรับค่าพิกัดที่อ่านได้จากเครื่อง
โดย Location Listener จะมี Method ที่ชื่อว่า onLocationChange(...) ด้วย เพื่อบอกให้รู้ว่าได้พิกัดของเครื่องแล้ว โดยค่าที่ส่งมาจะอยู่ในคลาส Location อีกที
กลับมาที่โค๊ดของเจ้าของบล็อก จะใช้วิธี Implement คลาส LocationListener ไว้ที่ MainActivity เลย ได้ออกมาประมาณนี้
จากตัวอย่างนี้จะอ่านพิกัดด้วยความแม่นยำสูงสุดทุกๆ 5 วินาที โดยจะส่งค่าที่อ่านมามาที่ onLocationChanged(...)
และถ้าอยากให้ Location Services หยุดทำงานก็มีคำสั่ง
จากตัวอย่างโค๊ดของเจ้าของบล็อก ถ้าอยากจะหยุดทำงานก็ใช้คำสั่งแบบนี้
แต่สำหรับ Location Services ไม่จำเป็นต้องทำแบบนั้น เพราะว่าตัวมันเองผูกเข้ากับ Google API Client อยู่แล้ว ถ้า Google API Client หยุดทำงาน มันก็จะหยุดทำงานเช่นกัน
ดังนั้นคำสั่ง disconnect ของ Google API Client ที่ใส่ไว้ใน onStop ก็จะเป็นการสั่งให้ Location Services หยุดทำงานไปในตัว
และเมื่ออยากให้กลับมาทำงานใหม่อีกครั้งก็ใช้แค่เพียงคำสั่ง connect() เท่านี้ Location Services ก็จะทำงานทันที เพราะในตัวอย่างนี้กำหนดให้ Location Services ทำงานเมื่อ Google API Client เชื่อมต่อเรียบร้อยแล้วนั่นเอง
เพิ่มเติม ในกรณีที่อยากให้ Location Services ทำงานเมื่อต้องการ เช่น กดปุ่ม ก็แค่ย้ายคำสั่ง Location Request และ Location Services ไปไว้ในที่ที่ต้องการแทน
• PRIORITY_HIGH_ACCURACY
ให้ประกาศ Permission เป็น
• PRIORITY_BALANCED_POWER_ACCURACY
• PRIORITY_LOW_POWER
• PRIORITY_NO_POWER
ให้ประกาศ Permission เป็น
ถ้าในแอปพลิเคชันมีการใช้งาน Location Provider ด้วย Priority ทั้งสองแบบก็สามารถประกาศ Permission คู่กันไว้ได้เลย
คลาส Location ไม่ได้มีแค่ค่าละติจูดหรือลองติจูดให้ใช้งานเท่านั้น แต่ยังมีข้อมูลอื่นๆที่ได้จาก Location Provider อีกด้วย เช่น ระดับความสูงจากผิวน้ำทะเล, ทิศทางอิงตามเข็มทิศ และความเร็วในการเคลื่อนที่ เป็นต้น
ดังนั้นโค้ดทั้งหมดก็จะออกมาประมาณนี้
และก็อย่าลืมว่าอาจจะมีกรณีที่ Location Provider ไม่สามารถใช้งานได้ อาจจะเพราะปิดไว้อยู่ ก็ควรให้แอปพลิเคชันแจ้งผู้ใช้ด้วยว่าไม่สามารถใช้งานได้ และถ้าจะให้ดีก็ให้ผู้ใช้สามารถเปิดไปที่หน้าเปิดใช้งาน Location Provider ได้ทันที
ในกรณีที่ Google API Client ไม่สามารถเชื่อมต่อได้ อาจจะเพราะมาจากเครื่องนั้นๆไม่มี Google Play Services (เครื่องที่ไม่มีนี่หายากมากเลยนะ) ดังนั้นในกรณีที่ใช้งาน Google Location Services API ไม่ได้ แอปพลิเคชันก็ควรจะต้องทำงานได้อยู่ อาจจะใช้คำสั่ง Location Provider ที่เขียนเองมาใช้งานแทน แต่ถ้าจะให้แนะนำก็ลองใช้ไลบรารีแทน BestLocationProvider-Android [GitHub]
Google Location Services API เป็น API จาก Google Play Services ที่ทาง Google ทำออกมาเพื่อให้เรียกใช้งาน Location Provider ได้สะดวกขึ้น
Location Provider การเรียกใช้งานให้เครื่องค้นหาพิกัดของตัวเครื่อง (เรียกกันบ้านๆว่า GPS)
Location Request คลาสสำหรับกำหนดรูปแบบการทำงานของ Location Provider
Location Services คลาสสำหรับสั่งให้ Location Provider ทำงาน
Location Listener เป็น Event Listener สำหรับ Location Services
Fused Provider = Network Provider + GPS Provider
เมื่อข้อดีและข้อเสียของทั้งคู่นั้นตรงข้ามกันอย่างสิ้นเชิง เพื่อให้ Location Provider ใช้งานได้อย่างมีประสิทธิภาพ จึงมีการจับ Provider ทั้ง 2 แบบมาทำงานร่วมกัน เพื่อให้ได้ตำแหน่งรวดเร็วที่สุดและแม่นยำที่สุดโดยในช่วงแรกๆที่ GPS Provider ยังระบุตำแหน่งไม่ได้ ก็จะเป็นการระบุตำแหน่งด้วย Network Provider แทนไปก่อน และเมื่อ GPS Provider จับตำแหน่งได้แม่นยำแล้วก็จะใช้ตำแหน่งจาก GPS Provider แทน
ซึ่งวิธีนี้มีประสิทธิภาพมาก ติดแค่อย่างเดียว
"เปลืองแบตมาก"
ปัญหาเดิมๆกับ Location Provider ที่ยังคงเป็นกันอยู่
ทุกวันนี้การเรียกใช้งาน Location Provider หรือที่ชอบเรียกกันว่า GPS ก็ยังมีนักพัฒนาหลายๆคนยังคงใช้คลาส Location ที่อยู่ใน android.location เพื่อเรียกใช้งาน Location ProviderLocation Provider เป็นการทำงานอีกอย่างหนึ่งที่ "กินแบต" และจะ "กินแบตมากขึ้น" ถ้าจัดการมันไม่ดีพอ ลองถามตัวเองดูว่าแอปพลิเคชันของคุณมีการใช้งาน Location Provider อยู่หรือไม่ และถ้าใช้อยู่ คิดว่าโค๊ดที่ใช้ในตอนนี้มันจัดการกับ Location Provider ได้ดีพอแล้วหรือป่าว?
ดีพอหรือป่าวไม่รู้ เพราะทำตามในอินเตอร์เน็ต แค่มันใช้งานได้ก็พอแล้วนี่?
นี่คือสาเหตุหลักที่ทำให้การใช้งาน Location Provider ไม่มีประสิทธิภาพ เพราะข้อมูลในอินเตอร์เน็ตแค่บอกว่าทำตามแล้วใช้งานได้ แต่ไม่ได้บอกว่ามันจะเป็น Best Practice ในการใช้งาน Location Provider
โค๊ดของผู้ที่หลงเข้ามาอ่านแค่สั่งให้มันทำงาน แล้วไม่ได้จัดการอะไรต่อ และมันยังคงทำงานอยู่ถึงแม้จะปิดแอปพลิเคชันไปแล้วใช่มั้ย?
เลิกเถอะครับ Location Provider ที่แย่ๆ
ในการใช้งาน Location Provider ไม่ได้มีแค่เรียกใช้งานตามต้องการแล้วก็จบ แต่ควรจัดการกับมันให้ดีๆด้วย เรียกใช้งานเฉพาะเวลาที่จำเป็น กำหนดความแม่นยำให้เหมาะสมกับงาน และอื่นๆ เพื่อไม่ให้แอปพลิเคชันที่ผู้ที่หลงเข้ามาอ่านสร้างขึ้นมา กลายเป็นแค่ตัวสูบแบตที่ทำให้ผู้ใช้ต้องกดลบทิ้งจึงทำให้ทีมแอนดรอยด์เข็น API สำหรับเรียกใช้งาน Location Provider ออกมาให้นักพัฒนาได้ใช้งานกัน โดยยัดมันลงไปใน Google Play Services โดยมีชื่อเรียกว่า Google Location Services API
ทำเรื่องยากให้เป็นเรื่องง่ายด้วย Google Location Services API
โดยปกติแล้ว Google Play Services ที่ติดตั้งอยู่ในแอนดรอยด์แทบจะทุกเครื่อง ณ ตอนนี้ก็มีการเรียก Location Provider เป็นระยะๆ เพื่อใช้เป็นข้อมูลในการทำงานสำหรับแอปพลิเคชันของ Google อยู่แล้ว ไม่ว่าจะเป็น Google Maps, Google Fit, Google Plus และอื่นๆอีกหลายตัวแต่ถ้าจะให้ทุกๆตัวเรียกใช้งาน Location Provider แยกกันก็คงทำให้สูบแบตไม่น้อย ดังนั้นเค้าจึงรวมเป็น API ไว้ใน Google Play Services ซะเลย เพื่อที่ว่าจะได้เรียกใช้งานจากที่เดียวเลย รวมไปถึงเปิดให้นักพัฒนาแอปฯต่างๆเข้าใช้งานได้ด้วย
ซึ่ง Google Location Services API มีข้อดีก็ตรงที่นักพัฒนาไม่ต้องจัดการเรื่อง Battery/Performance Optimizing เลย เพราะ API ตัวนี้จัดการให้หมดแล้ว
เริ่มใช้งาน Google Location Services API
ก่อนอื่นเริ่มจากเพิ่ม Dependencies ของ Google Location Services API ลงใน build.gradle ก่อนเลยcompile 'com.google.android.gms:play-services-location:12.0.0'
และ Permission ที่ต้องประกาศมีเพียงแค่ 2 ตัว ที่ไม่จำเป็นต้องประกาศทั้ง 2 ตัว
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
ประกาศแค่ตัวใดตัวหนึ่ง?
ใช่ครับ ขึ้นอยู่กับว่าต้องการความแม่นยำของตำแหน่งผู้ใช้งานมากน้อยแค่ไหน ซึ่งเดี๋ยวเจ้าของบล็อกจะอธิบายให้ฟังทีหลังนะ ตอนนี้ประกาศทั้ง 2 ตัวไปก่อนก็ได้
เพิ่มเติม ที่ต้องทำเพิ่มก็คือ Runtime Permission ซึ่งเป็นฟีเจอร์ของ Android 6.0 Marshmallow ที่นักพัฒนาต้องเขียนเพิ่มเพื่อขออนุญาตผู้ใช้เพื่อเข้าถึง Location Service ในตัวเครื่องได้ Google Play Services and Runtime Permissions [Google APIs for Android]
เชื่อมต่อกับ Google Api Client ก่อนทุกครั้ง
ในการใช้งาน API ใดๆใน Google Play Service ส่วนใหญ่จะต้องเชื่อมต่อกับ Google API Client ก่อนทุกครั้ง (แต่ Google Maps API ไม่ต้อง) โดยจะมีรูปแบบคำสั่งดังนี้val googleApiClient = GoogleApiClient.Builder(context)
.addApi(LocationServices.API)
.addConnectionCallbacks(connectionCallback)
.addOnConnectionFailedListener(connectionFailedListener)
.build()
googleApiClient.connect()
นี่คือคำสั่งสำหรับการเชื่อมต่อ Google API Client เพื่อใช้งาน Google Location Services API นะครับ ถ้าจะเชื่อมต่อกับบริการอย่างอื่นด้วยก็อาจจะต้องมีคำสั่งอื่นเพิ่มเข้ามาด้วย
เมื่อเอาไปใช้งานจริงๆก็จะเป็นแบบนี้ (ยาวหน่อยนะ)
// MainActivity.kt
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.api.GoogleApiClient
import com.google.android.gms.location.LocationServices
class MainActivity : AppCompatActivity, GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener {
private lateinit var googleApiClient: GoogleApiClient
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
googleApiClient = GoogleApiClient.Builder(this)
.addApi(LocationServices.API)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.build()
}
override fun onStart() {
super.onStart()
googleApiClient.connect()
}
override fun onStop() {
super.onStop()
if (googleApiClient.isConnected) {
googleApiClient.disconnect()
}
}
override fun onConnected(connectionHint: Bundle?) {
// Do something when connected with Google API Client
}
override fun onConnectionSuspended(cause: Int) {
// Do something when Google API Client connection was suspended
}
override fun onConnectionFailed(result: ConnectionResult) {
// Do something when Google API Client connection failed
}
}
ในการเชื่อมต่อกับ Google API Client จะต้องมีการกำหนดค่าต่างๆก่อน เช่น
addApi(...) กำหนดว่าจะเชื่อมต่อกับ API ตัวไหนของ Google Play Services
addConnectionCallbacks(...) กำหนด Callback สำหรับสถานะการเชื่อมต่อกับ Google API Client
addConnectionFailedListener(...) กำหนด Listener เมื่อเชื่อมต่อกับ Google API Client ไม่สำเร็จ
แล้วจบท้ายด้วยคำสั่ง build() เพื่อสร้าง Instance ของ Google API Client ขึ้นมา และเริ่มเชื่อมต่อด้วยคำสั่ง connect()
ส่วน Callback และ Listener เจ้าของบล็อกประกาศ Implement ไว้ที่ MainActivity ไปเลย ซึ่งจะได้ Method มาทั้งหมด 3 ตัวด้วยกัน
onConnected(...) เป็น Method ของ ConnectionCallback ทำงานเมื่อเชื่อมต่อกับ Google API Client ได้สำเร็จ
onConnectionSuspended(...) เป็น Method ของ ConnectionCallback ทำงานเมื่อการเชื่อมต่อกับ Google API Client ถูกยกเลิกกลางคัน
onConnectionFailed(...) เป็น Method ของ OnConnectionFailedListener ทำงานเมื่อไม่สามารถเชื่อมต่อกับ Google API Client
รวมไปถึง Handle การเชื่อมต่อกับ Google API Client ในเวลาที่แอปพลิเคชันเกิด onStart() และ onStop() ด้วย โดยย้ายคำสั่ง connect() ไปไว้ใน onStart() เพื่อให้เชื่อมต่อทุกครั้งเวลาที่กลับเข้ามาใช้งานแอปพลิเคชัน และใช้คำสั่ง disconnect() เพื่อยกเลิกการเชื่อมต่อทุกครั้งใน onStop() หรือ onDestroy() (ออกจากแอปพลิเคชัน)
เรียกใช้งาน Location Services เมื่อเชื่อมต่อกับ Google API Client ได้แล้ว
เรียกใช้งานตรงไหนรู้มั้ยครับ?ก็แหงสิ ต้องเรียกใช้งาน Location Services ใน onConnected(...) แล้วสิ ส่วน Method อีกสองตัวที่เหลือก็มีไว้ Handle เมื่อ Google API Client ใช้งานไม่ได้ (ตรงนี้เจ้าของบล็อกจะไม่พูดถึง)
ก่อนจะใช้งาน Location Services ทุกครั้งให้ตรวจสอบก่อนว่า Location Provider บนอุปกรณ์แอนดรอยด์เครื่องน้ันๆเปิดอยู่หรือไม่ เพราะผู้ใช้หลายๆคนชอบปิดเพื่อประหยัดแบตกัน
val locationAvailability = LocationServices.FusedLocationApi.getLocationAvailability(googleApiClient)
if(locationAvailability.isLocationAvailable) {
// Call Location Services
} else {
// Do something when Location Provider is not available
}
ถ้าพร้อมใช้งานแล้วก็ให้ใช้คำสั่ง Location Services ได้เลย แต่ถ้าไม่ได้เปิดให้ใช้งานก็ให้แจ้งผู้ใช้งานซะว่า Location Provider ใช้ไม่ได้นะ หรือจะทำให้กดเพื่อไปหน้าเปิดใช้งาน Location Provider ก็ได้นะ เดี๋ยวพอผู้ใช้กดเปิดแล้วกลับมาเข้าแอปพลิเคชัน onStart() ก็จะทำงานอีกครั้งและ Google API Client ก็จะเชื่อมต่อใหม่โดยอัตโนมัติเอง
เมื่อ Location Provider พร้อมใช้งานแล้วก็เรียก Location Services ด้วยคำสั่งประมาณนี้
val locationRequest = LocationRequest.create().apply {
priority = LocationRequest.PRIORITY_HIGH_ACCURACY
interval = 5000
}
LocationServices.FusedLocationApi.requestLocationUpdates(googleApiClient, locationRequest, locationListener)
สิ่งแรกที่ควรมีเลยคือ Location Request เพื่อตั้งค่าสำหรับ Location Provider เช่น ความแม่นยำ ระยะเวลาในการทำงาน อ่านพิกัดได้กี่ครั้งแล้วหยุดทำงาน เป็นต้น
fun setExpirationDuration(millis: Long) : LocationRequest
fun setExpirationTime(millis: Long) : LocationRequest
fun setFastestInterval(millis: Long) : LocationRequest
fun setInterval(millis: Long) : LocationRequest
fun setNumUpdates(numUpdates: Int) : LocationRequest
fun setPriority(priority: Int) : LocationRequest
fun setSmallestDisplacement(smallestDisplacementMeters: Float) : LocationRequest
setExpirationDuration(millis: Long) กำหนดว่าจะให้อ่านพิกัดเป็นระยะเวลานานเท่าไร (หน่วยเป็นมิลลิวินาที)
setExpirationTime(millis: Long) กำหนดว่าจะให้อ่านพิกัดจนถึงเมื่อไร โดยนับเวลาตั้งแต่เปิดเครื่องขึ้นมา (หน่วยเป็นมิลลิวินาที)
setFastestInterval(millis: Long) กำหนดระยะเวลาในการอ่านพิกัดแต่ละครั้งที่เร็วที่สุดเท่าที่ทำได้ (หน่วยเป็นมิลลิวินาที)
setInterval(millis: Long) กำหนดระยะเวลาในการอ่านพิกัดแต่ละครั้ง (หน่วยเป็นมิลลิวินาที)
setNumUpdates(numUpdates: Int) จำนวนครั้งในการอ่านพิกัด
setPriorty(priority: Int) กำหนดความสำคัญในการอ่านข้อมูล ซึ่งมีผลไปถึงการใช้แบตเตอรีของเครื่องด้วย
setSmallestDisplacement(smallestDisplacementMeters: Float) ระยะทางขั้นต่ำที่จะให้อ่านพิกัดใหม่อีกครั้ง
Location Request จะช่วยให้กำหนดขอบเขตการทำงานของ Location Provider ได้ เพราะในบางครั้งแอปพลิเคชันอาจจะไม่จำเป็นต้องรู้พิกัดแม่นยำมากก็ได้ เช่น แค่อยากรู้ว่าอยู่เขตไหนในกรุงเทพ เป็นต้น แต่คำสั่งสำคัญที่อยากให้ดูกันก็คือ setPriority(...) ที่เอาไว้กำหนดความแม่นยำในการทำงาน
• LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY เน้นการอ่านพิกัดโดยใช้พลังงานที่เหมาะสม แต่ก็ได้ความแม่นยำพอสมควร โดยพิกัดที่ได้จะมีความแม่นยำในระยะ 100 เมตร (รู้ว่าอยู่ถนนอะไร)
• LocationRequest.PRIORITY_HIGH_ACCURACY เน้นการอ่านพิกัดโดยให้ได้ความแม่นยำสูงสุดเท่าที่ทำได้ ซึ่งจะเปลืองแบตมากที่สุด (รู้ว่ายืนอยู่ตรงไหน)
• LocationRequest.PRIORITY_LOW_POWER เน้นการอ่านพิกัดโดยใช้พลังงานน้อยที่สุด ความแม่นยำจะต่ำมาก คลาดเคลื่อนได้มากถึง 10 กิโลเมตร (อยู่ว่าอยู่เขตไหน ตำบลไหน)
• LocationRequest.PRIORITY_NO_POWER เป็นการอ่านพิกัดแบบ Passive คือไม่มีการสั่งให้ Google Play Services อ่านพิกัด แต่จะรอจนว่า Google Play Services จะอ่านพิกัดตามการทำงานปกติจึงค่อยนำข้อมูลนั้นมา Update ซึ่งการทำงานแบบนี้จะไม่ทำให้ใช้พลังงานเพิ่ม แต่ Interval ของข้อมูลจะนานมาก
ยกตัวอย่างการกำหนด Location Request
import com.google.android.gms.location.LocationRequest
...
var locationRequest = LocationRequest.create().apply {
priority = LocationRequest.PRIORITY_HIGH_ACCURACY
interval = 5000
fastestInterval = 1000
}
จากตัวอย่างนี้กำหนดว่าอ่านพิกัดแบบแม่นยำที่สุด โดยจะอ่านพิกัดทุกๆ 5 วินาที แต่ถ้าเครื่องสามารถจับพิกัดได้ก่อน ก็จะอ่านพิกัดทันทีทุกๆ 1 วินาที
และสมมติว่ากำหนดแค่ Priority ล่ะ?
import com.google.android.gms.location.LocationRequest
...
LocationRequest mRequest = LocationRequest.create().apply {
priority = LocationRequest.PRIORITY_LOW_POWER
}
ถ้ากำหนด Location Request แบบนี้จะทำให้ Location Provider อ่านพิกัดเพียงแค่ครั้งเดียวเท่านั้นแล้วก็หยุดทำงาน เพราะว่าไม่มีการกำหนด Interval
ดังนั้นควรถ้าต้องการให้อ่านพิกัดตลอดเวลาก็อย่าลืมกำหนด Interval ด้วยนะครับ
หลังจากกำหนด Location Request เสร็จเรียบร้อยแล้วก็จะเรียกใช้งาน Location Services ด้วยคำสั่ง
LocationServices.FusedLocationApi.requestLocationUpdates(client: GoogleApiClient, request: LocationRequest, listener: LocationListener)
จะเห็นว่าต้องกำหนด Google API Client และ Location Request ซึ่งประกาศไว้เรียบร้อยแล้ว เอามากำหนดในคำสั่งนี้ได้เลย และในการทำงานของ Location Services จะมี Listener ที่ชื่อว่า Location Listner เพื่อรับค่าพิกัดที่อ่านได้จากเครื่อง
โดย Location Listener จะมี Method ที่ชื่อว่า onLocationChange(...) ด้วย เพื่อบอกให้รู้ว่าได้พิกัดของเครื่องแล้ว โดยค่าที่ส่งมาจะอยู่ในคลาส Location อีกที
val listener = object : LocationListener {
override fun onLocationChanged(location : Location?) {
// Do something when got new current location
}
}
กลับมาที่โค๊ดของเจ้าของบล็อก จะใช้วิธี Implement คลาส LocationListener ไว้ที่ MainActivity เลย ได้ออกมาประมาณนี้
// MainActivity.kt
import android.location.Location
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import com.google.android.gms.common.api.GoogleApiClient
import com.google.android.gms.location.LocationAvailability
import com.google.android.gms.location.LocationListener
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationServices
class MainActivity: AppCompatActivity(), GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener, LocationListener {
...
override fun onConnected(connectionHint: Bundle?) {
var locationAvailability = LocationServices.FusedLocationApi.getLocationAvailability(googleApiClient)
if(locationAvailability.isLocationAvailable) {
val locationRequest = LocationRequest.create().apply {
priority = LocationRequest.PRIORITY_HIGH_ACCURACY
interval = 5000
}
LocationServices.FusedLocationApi.requestLocationUpdates(googleApiClient, locationRequest, this)
} else {
// Do something when location provider not available
}
}
...
override fun onLocationChanged(location: Location?) {
// Do something when got new current location
}
}
จากตัวอย่างนี้จะอ่านพิกัดด้วยความแม่นยำสูงสุดทุกๆ 5 วินาที โดยจะส่งค่าที่อ่านมามาที่ onLocationChanged(...)
และถ้าอยากให้ Location Services หยุดทำงานก็มีคำสั่ง
removeLocationUpdates(client: GoogleApiClient, listener: LocationListener)
จากตัวอย่างโค๊ดของเจ้าของบล็อก ถ้าอยากจะหยุดทำงานก็ใช้คำสั่งแบบนี้
removeLocationUpdates(googleApiClient, this)
ต้องหยุด Location Services เมื่อแอปพลิเคชันหยุดทำงานหรือไม่?
เนื่องจาก Location Services จะทำงานเรื่อยๆตามที่กำหนดไว้ ดังนั้นถ้าแอปพลิเคชันหยุดทำงาน Location Services ก็ควรจะหยุดทำงานเช่นกันใช่มั้ยล่ะครับแต่สำหรับ Location Services ไม่จำเป็นต้องทำแบบนั้น เพราะว่าตัวมันเองผูกเข้ากับ Google API Client อยู่แล้ว ถ้า Google API Client หยุดทำงาน มันก็จะหยุดทำงานเช่นกัน
ดังนั้นคำสั่ง disconnect ของ Google API Client ที่ใส่ไว้ใน onStop ก็จะเป็นการสั่งให้ Location Services หยุดทำงานไปในตัว
override fun onStart() {
super.onStart()
googleApiClient.connect()
}
override fun onStop() {
super.onStop()
if (googleApiClient.isConnected) {
googleApiClient.disconnect()
}
}
และเมื่ออยากให้กลับมาทำงานใหม่อีกครั้งก็ใช้แค่เพียงคำสั่ง connect() เท่านี้ Location Services ก็จะทำงานทันที เพราะในตัวอย่างนี้กำหนดให้ Location Services ทำงานเมื่อ Google API Client เชื่อมต่อเรียบร้อยแล้วนั่นเอง
เพิ่มเติม ในกรณีที่อยากให้ Location Services ทำงานเมื่อต้องการ เช่น กดปุ่ม ก็แค่ย้ายคำสั่ง Location Request และ Location Services ไปไว้ในที่ที่ต้องการแทน
กำหนด Permission แค่ตัวไหนตัวหนึ่งตามที่จะใช้งานพอ
จากที่เจ้าของบล็อกบอกในตอนแรกว่า Permission สำหรับ Location Provider มีด้วยกัน 2 ตัว แต่ทว่าไม่ต้องประกาศทั้งคู่ ให้ดูว่า Priority ที่กำหนดใน Location Request เป็นแบบไหน• PRIORITY_HIGH_ACCURACY
ให้ประกาศ Permission เป็น
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
• PRIORITY_BALANCED_POWER_ACCURACY
• PRIORITY_LOW_POWER
• PRIORITY_NO_POWER
ให้ประกาศ Permission เป็น
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
ถ้าในแอปพลิเคชันมีการใช้งาน Location Provider ด้วย Priority ทั้งสองแบบก็สามารถประกาศ Permission คู่กันไว้ได้เลย
อ่านค่าพิกัดได้แล้วก็เอาไปใช้งานได้เลย
จากที่บอกไปว่าเมื่อได้ค่าพิกัดแล้ว Method ที่ชื่อว่า onLocationChanged(...) จะทำงานทันที โดยจะมีคลาส Location ให้ดึงข้อมูลไปใช้งานoverride fun onLocationChanged(location: Location?) {
var provider: String = location.provider
var latitude: Double = location.latitude
var longitude: Double = location.longitude
var altitude: Double = location.altitude
var accuracy: Float = location.accuracy
var bearing: Float = location.bearing
var speed: Float = location.speed
var time: Long = location.time
}
คลาส Location ไม่ได้มีแค่ค่าละติจูดหรือลองติจูดให้ใช้งานเท่านั้น แต่ยังมีข้อมูลอื่นๆที่ได้จาก Location Provider อีกด้วย เช่น ระดับความสูงจากผิวน้ำทะเล, ทิศทางอิงตามเข็มทิศ และความเร็วในการเคลื่อนที่ เป็นต้น
ดังนั้นโค้ดทั้งหมดก็จะออกมาประมาณนี้
// MainActivity.kt
import android.location.Location
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.TextView
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.api.GoogleApiClient
import com.google.android.gms.location.LocationAvailability
import com.google.android.gms.location.LocationListener
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationServices
class MainActivity : AppCompatActivity, GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener, LocationListener {
private lateinit val googleApiClient GoogleApiClient
override fun onCreate(savedInstanceState: Bundle?) {
...
// Create Google API Client instance
googleApiClient = GoogleApiClient.Builder(this)
.addApi(LocationServices.API)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.build()
}
override fun onStart() {
...
// Connect to Google API Client
googleApiClient.connect()
}
override fun onStop() {
...
if (googleApiClient.isConnected) {
// Disconnect Google API Client if available and connected
googleApiClient.disconnect()
}
}
@Override
public void onConnected(Bundle bundle) {
// Do something when connected with Google API Client
val locationAvailability = LocationServices.FusedLocationApi.getLocationAvailability(googleApiClient)
if (locationAvailability.isLocationAvailable) {
// Call Location Services
val locationRequest = LocationRequest.create().apply {
priority = LocationRequest.PRIORITY_HIGH_ACCURACY
interval = 5000
}
LocationServices.FusedLocationApi.requestLocationUpdates(googleApiClient, locationRequest, this)
} else {
// Do something when Location Provider not available
}
}
override fun onConnectionSuspended(cause: Int) {
// Do something when Google API Client connection was suspended
}
override fun onConnectionFailed(result: ConnectionResult) {
// Do something when Google API Client connection failed
}
override fun onLocationChanged(location: Location) {
// Do something when got new current location
location?let {
val location = """Latitude : ${location.latitude}
|Longitude : ${location.longitude}
""".trimMargin()
}
}
}
สรุป
สำหรับ Google Location Services API ถือว่าเป็นอีกหนึ่ง API ที่แนะนำให้ลองใช้ เพื่อที่การทำงานของ Location Provider จะได้มีประสิทธิภาพมากขึ้น โดยที่ผู้ที่หลงเข้ามาอ่านไม่จำเป็นต้อง Handle เอง ซึ่งในบทความนี้จะเป็นการแนะนำวิธีการใช้งานเบื้องต้น ดังนั้นลองศึกษาเพิ่มเติมได้จาก Location & Context [Android Developer] เพื่อให้ใช้งาน Google Location Services API ได้ตรงกับงานของผู้ที่หลงเข้ามาอ่านและก็อย่าลืมว่าอาจจะมีกรณีที่ Location Provider ไม่สามารถใช้งานได้ อาจจะเพราะปิดไว้อยู่ ก็ควรให้แอปพลิเคชันแจ้งผู้ใช้ด้วยว่าไม่สามารถใช้งานได้ และถ้าจะให้ดีก็ให้ผู้ใช้สามารถเปิดไปที่หน้าเปิดใช้งาน Location Provider ได้ทันที
ในกรณีที่ Google API Client ไม่สามารถเชื่อมต่อได้ อาจจะเพราะมาจากเครื่องนั้นๆไม่มี Google Play Services (เครื่องที่ไม่มีนี่หายากมากเลยนะ) ดังนั้นในกรณีที่ใช้งาน Google Location Services API ไม่ได้ แอปพลิเคชันก็ควรจะต้องทำงานได้อยู่ อาจจะใช้คำสั่ง Location Provider ที่เขียนเองมาใช้งานแทน แต่ถ้าจะให้แนะนำก็ลองใช้ไลบรารีแทน BestLocationProvider-Android [GitHub]
คำศัพท์กันงง
เนื่องจากมีคำหลายคำที่เจ้าของบล็อกใช้ในบทความนี้ อาจจะทำให้ผู้ที่หลงเข้ามาอ่านบางคนงงว่ามันคืออะไร ดังนั้นเจ้าของบล็อกจึงขออธิบายคำศัพท์เหล่านี้ให้เข้าใจมากขึ้นนะครับGoogle Location Services API เป็น API จาก Google Play Services ที่ทาง Google ทำออกมาเพื่อให้เรียกใช้งาน Location Provider ได้สะดวกขึ้น
Location Provider การเรียกใช้งานให้เครื่องค้นหาพิกัดของตัวเครื่อง (เรียกกันบ้านๆว่า GPS)
Location Request คลาสสำหรับกำหนดรูปแบบการทำงานของ Location Provider
Location Services คลาสสำหรับสั่งให้ Location Provider ทำงาน
Location Listener เป็น Event Listener สำหรับ Location Services