ใช้งาน Google Maps Direction API ให้ง่ายๆขึ้นด้วย Google Direction Library

โดยปกติแล้วการนำแผนที่ของ Google Maps มาแสดงบนแอปพลิเคชันแอนดรอยด์นั้น ทาง Google จะทำ Library ไว้ให้อยู่แล้ว จึงทำให้สะดวกต่อนักพัฒนาแอนดรอยด์ที่ต้องการนำมาใช้งาน และถ้าต้องการใช้งาน Google Place API ก็มี Library ให้แล้วเช่นกัน

แต่ทว่าถ้าอยากจะให้แสดงเส้นทางเพื่อนำทางบน Google Maps ก็จะต้องใช้ Google Maps Direction API ซึ่งปัญหาก็คือตอนนี้ทาง Google ยังไม่ได้ Provide เจ้า API ตัวนี้ให้แอนดรอยด์โดยเฉพาะ ดังนั้นจึงต้องใช้วิธีดึงข้อมูลจาก API ผ่าน HTTP แบบตรงๆกันไปก่อน และข้อมูลที่ได้มานั้นก็จะเป็น JSON (หรือ XML) ซึ่งนักพัฒนาก็ต้องมานั่งแยกข้อมูลอีกที ก่อนที่จะทำไปใช้งาน

ดังนั้นเพื่อให้สะดวกมากขึ้นเจ้าของบล็อกจึงเขียน Library ขึ้นมาเพื่อช่วยให้สามารถเรียกใช้งาน Google Maps Direction API ได้ง่ายขึ้น อย่างน้อยก็จนกว่าทาง Google จะ Provide Library สำหรับแอนดรอยด์มาให้น่ะแหละ

เกี่ยวกับ Google Direction Library

Google Direction Library ของเจ้าของบล็อกสร้างขึ้นมาเพื่อใช้งานบนแอนดรอยด์โดยเฉพาะ และผูกกับ Google Maps ดังนั้นถ้าตัว Library ก็จะมี Dependencies ของ Google Play Service (Maps) ด้วย นั่นก็หมายความว่า Library ตัวนี้ต้องใช้ Google Play Services ด้วย และเบื้องหลังการทำงานก็จะใช้ Retrofit (2.0), GSON และ Parceler ด้วย

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

และ Library ตัวนี้ต้องใช้ API Key จาก Google Developer Console ด้วย คล้ายๆกับ Google Maps for Android เลย แต่ทว่าประเภทของ Key จะต่างกันไปหน่อย เพราะว่าของ Google Maps สามารถขอเป็น Android Key ได้เลย เนื่องจากมี Library สำหรับแอนดรอยด์ แต่ Google Direction Library นั้นใช้วิธีการติดต่อผ่าน HTTP ธรรมดาๆ ดังนั้นจึงต้องขอ Key เป็นแบบ Server Key ครับ

ก่อนจะขอ Server Key ก็อย่าลืมเปิดใช้งาน Google Maps Direction API ก่อนนะ

ซึ่งขั้นตอนการขอ Server Key ก็เหมือนกับ Android Key เลย เพียงแต่เลือกคนละอันกันเท่านั้น

 

 

จะตั้งชื่อว่าอะไรก็ได้ ขอแค่ให้รู้ก็พอว่ามันคืออะไร ส่วนช่อง IP Address ไม่ต้องกรอกอะไรลงไป ปล่อยโล่งๆไว้นั้นแหละ เอาแล้ว API Key ไปใช้งานได้เลย

การใช้งาน Google Direction Library

สามารถดึงผ่าน Maven ได้เลย โดยใส่เป็น Dependencies ไว้ใน build.gradle แบบนี้

dependencies {
    ...
    compile 'com.akexorcist:googledirectionlibrary:1.0.5'
}

และคำสั่งในการเรียกใช้งานก็แสนเรียบง่าย (เพราะความวุ่นวายมันอยู่ตอนที่ข้อมูลส่งกลับมา) โดยรูปแบบคำสั่งที่ง่ายที่สุดจะเป็นแบบนี้

GoogleDirection.withServerKey(String serverKey)
    .from(LatLng origin)
    .to(LatLng destination)
    .execute(DirectionCallback callback);

serverKey ก็คือ API Key ที่ขอมาจาก Google Developer Console
origin คือพิกัดเริ่มต้นที่จะให้นำทาง โดยอยู่ในรูปของคลาส LatLng (ของ Google Maps)
destination คือพิกัดปลายทางที่จะให้นำทาง
callback คือ Direction Callback Event ที่จะถูกเรียกเมื่อข้อมูลจาก Google Maps Direction API ส่งกลับมาแล้ว

เวลาใช้งานจริงๆก็จะเป็นแบบนี้

String serverKey = "AIzaSyDFWdlR5DG1VYXSaMwG62ilxxxxxxxxx";
LatLng origin = new LatLng(37.7849569, -122.4068855);
LatLng destination = new LatLng(37.7814432, -122.4460177);
GoogleDirection.withServerKey(serverKey)
    .from(origin)
    .to(destination)
    .execute(new DirectionCallback() {
        @Override
        public void onDirectionSuccess(Direction direction, String rawBody) {
            // Do something here 
        }

        @Override
        public void onDirectionFailure(Throwable t) {
            // Do something here 
        }
    });

โดย Callback จะมีสองแบบด้วยกัน คือ Success กับ Failure ถ้ารับข้อมูลได้สำเร็จก็จะได้ Object กลับมาเป็น Direction Instance ก็ดึงข้อมูลจากในนั้นไปใช้งานได้เลย หรือจะเอาเป็นแบบ String ไปใช้งานก็ได้

แต่ถ้าเกิดข้อผิดพลาดเช่น ไม่ได้เชื่อมต่ออินเตอร์เน็ตหรือ Connection Timeout ก็จะส่งกลับมาเป็น Failure ส่วนสาเหตุก็ให้วิเคราะห์จากใน Throwable Instance ต่อไป เพื่อแสดงข้อความแจ้งให้ผู้ใช้ทราบ

Optional Parameter สำหรับกำหนดข้อมูลที่ต้องการเพิ่มเติม

นอกจากคำสั่ง withServerKey, from และ to ที่ใช้สำหรับกำหนดข้อมูลที่ต้องการแล้ว ยังสามารถกำหนดอย่างอื่นเพิ่มเติมได้อีกด้วย โดยกำหนดหลังจากคำสั่ง to ก่อนที่จะใช้คำสั่ง execute

โดยจะมีดังนี้

transportMode(String transportMode)
language(String language)
unit(String unit)
avoid(String avoid)
transitMode(String transitMode)
alternativeRoute(boolean alternative)

 

Transport Mode

การกำหนดรูปแบบในการเดินทาง ให้กำหนดค่าโดยใช้ค่าจากคลาส TransportMode

TransportMode.DRIVING
TransportMode.WALKING
TransportMode.BICYCLING
TransportMode.TRANSIT

ซึ่งเส้นทางก็จะแตกต่างกันไปตามรูปแบบการเดินทาง ถ้าเป็น Bicycling ก็จะมีให้ในบางที่เท่านั้น และถ้าเป็น Transit ก็จะมีเส้นทางแบบ Walking ผสมอยู่ด้วย เพื่อให้เดินไปขึ้นรถโดยสารนั่นเอง

GoogleDirection.withServerKey(serverKey)
    .from(...)
    .to(...)
    .transportMode(TransportMode.WALKING)
    .execute(...);

 

Language

กำหนดรูปแบบภาษาของข้อมูลที่ส่งกลับมาจาก Google Maps Direction API ให้กำหนดค่าโดยใช้ค่าจากคลาส Language

Language.GERMAN
Language.ENGLISH
Language.KOREAN
Language.JAPAN
...

โดยข้อมูลที่ได้ก็จะเป็นภาษาที่ต้องการ แต่ทว่าก็ต้องคำนึงถึงสถานที่ด้วย เพราะควรจะเป็นภาษาที่ตรงกับพื้นที่ของเส้นทางนั้นๆ เช่น นักพัฒนาต้องการข้อความภาษาไทยในต่างประเทศ ก็อาจจะไม่ได้เป็นภาษาไทยเสมอไป เพราะไม่รองรับในประเทศนั้นๆ เป็นต้น

GoogleDirection.withServerKey(serverKey)
    .from(...)
    .to(...)
    .language(Language.THAILAND)
    .execute(...);

 

Unit

กำหนดหน่วยของระยะทางที่จะแสดงในข้อมูล ให้กำหนดค่าโดยใช้ค่าจากคลาส Unit

Unit.METRIC
Unit.IMPERIAL

METRIC  หน่วยกิโลเมตรและเมตร
IMPERIAL ก็จะได้เป็นหน่วยไมล์และฟุต

GoogleDirection.withServerKey(serverKey)
    .from(...)
    .to(...)
    .unit(Unit.METRIC)
    .execute(...);

 

Avoid

กำหนดว่าจะหลีกเลี่ยงเส้นทางแบบไหน ให้กำหนดค่าโดยใช้ค่าจากคลาส AvoidType

AvoidType.TOLLS
AvoidType.HIGHWAYS
AvoidType.FERRIES
AvoidType.INDOOR

TOLLS  หลีกเลี่ยงเส้นทางที่ต้องใช้ทางด่วน
HIGHWAYS หลีกเลี่ยงเส้นทางที่ต้องใช้ทางหลวง
FERRIES หลีกเลี่ยงเส้นทางที่ต้องเดินทางด้วยเรือ
INDOOR หลีกเลี่ยงเส้นทางที่ต้องเดินทางภายในอาคารหรือที่ร่ม
ซึ่งคำสั่งนี้สามารถกำหนดได้มากกว่าหนึ่งอย่าง เพียงแค่กำหนดค่าที่ต้องการซ้ำลงไป เพราะตัวคำสั่งเป็นการ Add ค่าเข้าไปจากของเดิม

GoogleDirection.withServerKey(serverKey)
    .from(...)
    .to(...)
    .avoid(AvoidType.FERRIES)
    .avoid(AvoidType.INDOOR)
    .execute(...);

 

Transit Mode

กำหนดประเภทของรถโดยสารที่ต้องการ เมื่อกำหนดเส้นทางเป็นแบบ Transit ให้กำหนดค่าโดยใช้ค่าจากคลาส TransitMode

TransitMode.BUS
TransitMode.SUBWAY
TransitMode.TRAIN
TransitMode.TRAM
TransitMode.RAIL

BUS  เลือกเป็นเส้นทางของรถบัส
SUBWAY เลือกเป็นเส้นทางของรถไฟใต้ดิน
TRAIN เลือกเป็นเส้นทางของรถไฟ
TRAM เลือกเป็นเส้นทางของรถราง
RAIL เลือกเป็นเส้นทางของรถไฟ, รถราง และรถไฟใต้ดิน (รวม SUBWAY, TRAIN และ TRAM)

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

GoogleDirection.withServerKey(serverKey)
    .from(...)
    .to(...)
    .transitMode(TransitMode.TRAIN)
    .transitMode(TransitMode.BUS)
    .transitMode(TransitMode.SUBWAY)
    .execute(...);

 

Alternative Route

กำหนดให้ส่งข้อมูลเส้นทางมามากกว่าหนึ่งเส้นทาง (ถ้ามี) โดยปกติแล้วตัว Google Maps Direction API จะส่งข้อมูลเส้นทางที่ดีที่สุดเท่านั้น แต่ถ้ากำหนดเป็นแบบ Alternative ก็จะส่งข้อมูลเส้นทางที่ที่เหมาะสมทั้งหมดมาให้

GoogleDirection.withServerKey(serverKey)
    .from(...)
    .to(...)
    .alternativeRoute(true)
    .execute(...);

โดยทั้งหมดนี้สามารถกำหนดร่วมกันได้ตามต้องการ อยากจะหลีกเลี่ยงเส้นทางแบบไหน อยากเดินทางแบบไหน ก็กำหนดพร้อมๆกันได้เลย

GoogleDirection.withServerKey(serverKey)
    .from(...)
    .to(...)
    .transportMode(TransportMode.TRANSIT)
    .transitMode(TransitMode.BUS)
    .unit(Unit.METRIC)
    .execute(...);

 

รูปแบบข้อมูลที่ส่งกลับมา

เมื่อข้อมูลถูกส่งกลับมาจาก Google Maps Direction API ก็จะส่งกลับมาในรูปของคลาส Direction ซึ่งข้อมูลก็จะค่อนข้างซับซ้อนพอสมควร แต่หลักๆที่ใช้งานก็จะมีดังนี้

ตรวจสอบสถานะของข้อมูล

เนื่องจากข้อมูลก็มีปัญหาได้ในบางครั้ง ดังนั้นควรตรวจสอบสถานะของข้อมูลก่อนว่าเป็นอะไร เพื่อที่ว่าจะได้จัดการให้ถูกต้องตามแต่ละเงื่อนไข ซึ่งคลาส Direction ก็จะมีคำสั่ง getStatus() ไว้ให้ดึงมาตรวจสอบอยู่แล้ว

@Override
public void onDirectionSuccess(Direction direction) {
    String status = direction.getStatus();
    // Do something
}

สำหรับเงื่อนไขทั้งหมดก็จะอยู่ในคลาส RequestResult ซึ่งมีทั้งหมดดังนี้

RequestResult.OK
RequestResult.NOT_FOUND
RequestResult.ZERO_RESULTS
RequestResult.MAX_WAYPOINTS_EXCEEDED
RequestResult.INVALID_REQUEST
RequestResult.OVER_QUERY_LIMIT
RequestResult.REQUEST_DENIED
RequestResult.UNKNOWN_ERROR

OK ข้อมูลถูกต้อง มีเส้นทางที่ต้องการ ผ่านฉลุย ลุยต่อได้เลย
NOT_FOUND ตำแหน่งพิกัดที่ระบุไม่ถูกต้อง
ZERO_RESULTS ไม่มีเส้นทางที่ต้องการค้นหา
MAX_WAYPOINTS_EXCEEDED กำหนด Waypoints มากเกินกำหนด ซึ่งใน Library นี้จะไม่มีปัญหานี้ เพราะว่าตอนนี้ยังไม่ได้เพิ่มคำสั่งให้กำหนด Waypoints ได้
INVALID_REQUEST กำหนด Parameter ในการค้นหาเส้นทางไม่ถูกต้อง ซึ่งใน Library นี้จะไม่มีปัญหานี้เช่นกัน
OVER_QUERY_LIMIT ใช้งาน Google Maps Direction API เกินโควต้า ซึ่งการใช้งานฟรีจะให้อยู่ที่ 2,500 Request ต่อวันเท่านั้น สามารถกำหนดใน Google Developer Console เพื่อจ่ายเงินสำหรับ Request ที่เกินโควต้าได้
REQUEST_DENIED ถูกยกเลิกข้อมูล อาจจะเพราะกำหนด API Key ผิด
UNKNOWN_ERROR เป็นข้อผิดพลาดที่ไม่สามารถระบุได้ ให้ลองเรียกใช้งานคำสั่งใหม่อีกครั้ง

ซึ่ง Status ที่ส่งมาให้นั้นเป็น String จึงสามารถเทียบกับแต่ละเงื่อนไขได้เลย

@Override
public void onDirectionSuccess(Direction direction) {
    String status = direction.getStatus();
    if(status.equals(RequestResult.OK)) {
        // Do something
    } else if(status.equals(RequestResult.NOT_FOUND)) {
        // Do something
    }
}

หรืออยากจะเปรียบเทียบง่ายๆว่าข้อมูลสำเร็จมั้ยก็มีคำสั่ง isOK ให้ใช้เลยนะ

@Override
public void onDirectionSuccess(Direction direction) {
    if(direction.isOK()) {
        // Do something
    }
}

 

Route

เส้นทางทั้งหมดจากต้นทางไปจนถึงปลายทาง ในกรณีที่มีเส้นทางที่สามารถเดินทางได้มากกว่าหนึ่งเส้นทางก็สามารถเลือกได้ แต่ตอนส่งข้อมูลไปที่ Google Maps Direction API ต้องกำหนดด้วยว่าขอข้อมูลเป็นแบบ Alternative Route

Waypoint

เนื่องจาก Google Maps Direction API นั้นสามารถกำหนดเส้นทางแบบต่อเนื่องได้ เช่น เดินทางจากบ้านไปร้านอาหาร แล้วเดินทางไปที่ทำงานต่อ จากเส้นทางตั้งแต่ต้นทางไปถึงปลายทาง ตำแหน่งระหว่างทางจะเรียกว่า Waypoint นั่นเอง

ซึ่งใน Library ตัวนี้ยังไม่สามารถกำหนด Waypoint ได้ ดังนั้นข้อมูลที่ได้จึงข้ามเรื่องนี้ไปได้เลยจ้า (ไว้อัพเดทเพิ่มเข้ามาให้)

Leg

เป็นคำที่ Google Maps Direction API ใช้เรียกเส้นทางจากจุดหนึ่งไปถึงอีกจุดหนึ่ง ในกรณีที่มี Waypoint ด้วย ก็จะมี Leg มากกว่าหนึ่งเช่นกัน

แต่เพราะ Library นี้ไม่มี Waypoint เพราะงั้น Leg ก็จะเหลือแค่อันเดียวนะ

Step

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

จากภาพตัวอย่างข้างต้นจะเห็นว่ามีทั้งหมด 7 Step

การดึงข้อมูลเส้นทาง

ข้อมูลเส้นทางที่ส่งมาทุกครั้ง ถ้ามีสถานะเป็น OK ก็จะมีข้อมูล Route อย่างน้อย 1 ชุด (ถ้ากำหนดเป็น Alternative Route ก็อาจจะได้ Route มากกว่า 1 ชุด) โดย Route จะอยู่ในรูปของ Array

ซึ่งใน Route 1 ชุดก็จะมี Leg 1 ชุด (เพราะ Library นี้ยังกำหนด Waypoint ไม่ได้) ถึงแม้ว่า Leg จะมีแค่ 1 ชุด แต่ก็อยู่ในรูป Array นะ

และใน Leg 1 ชุดก็จะมี Step อย่างน้อยหนึ่งชุด ยิ่งเส้นทางซับซ้อนมากเท่าไรก็ยิ่งเยอะมากเท่านั้น

ถ้าต้องการดึง Route มาใช้งาน สมมติว่ามี Route แค่ตัวเดียวก็จะใช้คำสั่งแบบนี้

Route route = direction.getRouteList().get(0);

และถ้าต้องการดึง Leg ที่อยู่ใน Route ก็จะคล้ายๆกันเลย

Route route = direction.getRouteList().get(0);
Leg leg = route.getLegList().get(0);

เนื่องจาก Step มักจะมีมากกว่า 1 ตัว ดังนั้นก็ให้ดึงมาทั้ง List ซะ

Route route = direction.getRouteList().get(0);
Leg leg = route.getLegList().get(0);
List<stepList> = leg.getStepList();

ซึ่งในแต่ละ Step ก็จะมีพิกัดเริ่มต้นและปลายทางของ Step นั้นๆที่สามารถดึงออกมาเป็นคลาส LatLng ที่ใช้ใน Google Maps ได้เลย

 

LatLng start = step.getStartLocation().getCoordination();
LatLng end = step.getEndLocation().getCoordination();

แต่เพื่อให้ง่ายขึ้น เจ้าของบล็อกจึงทำให้มันสามารถดึงค่าจาก Leg ออกมาเป็น LatLng Array ได้เลย จะได้ไม่ต้องไปใช้ For วนดึงค่าทีละตัว

ArrayList<LatLng> pointList = leg.getDirectionPoint();

และพิเศษสุดสำหรับการเดินทางแบบ Transit ก็จะพบว่า Step นั้นสามารถมี Step อยู่ซ้อนข้างในอีกชั้นได้

เพราะว่าการเดินทางแบบ Transit นั้นจะมี Walking ผสมอยู่ด้วย ดังนั้นจึงมีการบอก Step แยกให้สำหรับ Walking ด้วย ซึ่งก็คือ Step ที่ซ้อนอยู่ใน Step นั่นเอง

ซึ่งสามารถรู้ได้ว่า Step ไหนเป็นการเดินทางแบบไหนก็ให้ตรวขสอบ Travel Mode ของ Step นั้นๆ

String travelMode = step.getTravelMode();

แล้วก็เอาไปเปรียบกับค่าในคลาส TransportMode อีกที

ถ้าต้องการตำแหน่งของแต่ละ Step ก็สามารถดึง Section Point ออกมาจาก Leg เพื่อใช้งานได้เลย โดยส่งออกมาเป็น LatLng Array

ArrayList<LatLng> sectionList = leg.getSectionPoint();

ถ้าต้องการระยะทางหรือระยะเวลาในการเดินทางของเส้นทางทั้งหมดก็สามารถดึงข้อมูลได้จาก Leg เลย

Info distanceInfo = leg.getDistance();
Info durationInfo = leg.getDuration();
String distance = distanceInfo.getText();
String duration = durationInfo.getText();

แต่ถ้าอยากระยะทางหรือระยะเวลาในการเดินทางของแต่ละ Step ก็ให้ดึงข้อมูลจากแต่ละ Step ได้เลย

Info distanceInfo = step.getDistance();
Info durationInfo = step.getDuration();
String distance = distanceInfo.getText();
String duration = durationInfo.getText();

ถ้าอยากรู้ว่าต้องเดินทางไปทิศทางไหนก็ให้ดึงข้อมูลจาก Maneuver ที่อยู่ในแต่ละ Step แล้วเอาไปเปรียบเทียบกับค่าที่อยู่ในคลาส Maneuver อีกที หรือถ้าต้องการเป็นข้อความที่ใช้แสดงให้ผู้ใช้เห็นเลย สามารถดึงจาก HTML instruction ได้เลย

String maneuver = step.getManeuver();
String instruction = step.getHtmlInstruction();

แต่ว่า Instruction อยู่ในรูป HTML นะ ถ้าเอาไปแสดงก็อย่าลืม Format ให้เป็น HTML ด้วยนะ

อยากจะเอา Step ทั้งหมดไปแสดงบน Google Maps

สำหรับการวาดเส้นทางบน Google Maps จะต้องวาดเป็น Polyline โดยอยู่ในรูปของคลาส PolylineOptions ซึ่งใน Library ตัวนี้สามารถดึง LatLng Array แล้วแปลงเป็น PolylineOptions ได้ทันที เพราะมีคลาส DirectionConverter ให้ใช้งานแล้ว

ArrayList<LatLng> directionPositionList = leg.getDirectionPoint();
PolylineOptions polylineOptions = DirectionConverter.createPolyline(this, directionPositionList, 5, Color.RED);
googleMap.addPolyline(polylineOptions);

โดย 5 คือขนาดของเส้นในหน่วย DP และ Color.RED คือกำหนดเส้นเป็นสีแดง

จากตัวอย่างข้างต้นก็จะได้เส้นทางออกมาเป็นแบบนี้

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

การเดินทางแบบ Transit ให้ดึงค่า Step List ออกมาแทน แล้วใช้คำสั่งจากในคลาส DirectionConverter เพื่อแปลงข้อมูลเส้นทางแบบ Transit ให้ออกมาเป็น PolylineOptions Array แล้วจึงนำไปแปะลงบน Google Maps

List<Step> stepList = direction.getRouteList().get(0).getLegList().get(0).getStepList();
ArrayList<PolylineOptions> polylineOptionList = DirectionConverter.createTransitPolyline(this, stepList, 5, Color.RED, 3, Color.BLUE);
for (PolylineOptions polylineOption : polylineOptionList) {
    googleMap.addPolyline(polylineOption);
}

สำหรับคำสั่ง createTransitPolyline นั้นจะสามารถกำหนดเส้นทางของ Transit กับ Walking แยกกันได้ จากในตัวอย่างจะกำหนดให้เส้นทาง Transit มีขนาด 5dp เป็นสีแดง ส่วนเส้นทาง Walking มีขนาด 3dp เป็นสีน้ำเงิน

ในแต่ละ Step ของการเดินทางแบบ Transit จะสามารถดึงข้อมูลรายละเอียดของรถโดยสารนั้นๆเพิ่มเติมได้ โดยจะอยู่ในรูปของคลาส TransitDetail

TransitDetail transitDetail = step.getTransitDetail();

ซึ่งภายในคลาส TransitDetail ก็จะประกอบข้อมูลต่างๆไม่ว่าจะเป็นตำแหน่งที่ขึ้น/ลงรถโดยสาร ช่วงเวลาที่รถโดยสารมาถึงและถึงจุดหมาย และมีรายละเอียดเกี่ยวกับรถโดยสารนั้นๆเพื่อนำมาแสดงผลภายในแอพอีกด้วย โดยจะอยู่ในคลาส Line

StopPoint arrivalStopPoint = transitDetail.getArrivalStopPoint();
StopPoint departureStopPoint = transitDetail.getDepartureStopPoint();
TimeInfo arriveTimeInfo = transitDetail.getArrivalTime();
TimeInfo departureTimeInfo = transitDetail.getDepartureTime();
String headSign = transitDetail.getHeadsign();
String stopNumber = transitDetail.getStopNumber();
Line transitLine = transitDetail.getLine();

 

มีอะไรใหม่ในเวอร์ชัน 1.0.5

เจ้าของบล็อกได้อัปเดตไลบรารีตัวนี้เป็นเวอร์ชัน 1.0.5 แล้ว ซึ่งมีการแก้บั๊กและเพิ่มคลาส GoogleDirectionConfiguration เข้าไปเพื่อให้สามารถตั้งค่าบางอย่างสำหรับไลบรารีได้

แสดง Log เมื่อมีเรียกดึงข้อมูลเส้นทาง

GoogleDirectionConfiguration.getInstance().setLogEnabled(true);

ถ้าไม่มีการกำหนดค่าในคำสั่งนี้ จะมีค่า Default เป็น False

ใช้ Custom OkHttpClient ของผู้ที่หลงเข้ามาอ่าน

เพื่อให้ผู้ที่หลงเข้ามาอ่านสามารถปรับเปลี่ยนการทำงานของ OkHttpClient ได้ตามใจชอบ

OkHttpClient client = ....
GoogleDirectionConfiguration.getInstance().setCustomClient(client);

กำหนดค่าดังกล่าวเพียงแค่ครั้งเดียว ก่อนที่จะเรียกใช้คำสั่งดึงข้อมูลเส้นทาง

Demo

สามารถไปดาวน์โหลดลองดูกันก่อนได้ที่
Demo App for Google Direction Library [Play Store]

Source Code

ถ้าอยากลองศึกษา Source Code ของ Library ตัวนี้ก็เข้าไปส่องใน GitHub กันได้เลยจ้า
Google Direction Library [GitHub]

Reference Documentation

Intro : The Google Maps Directions API {Google Developers]

 

ref:http://www.akexorcist.com/2015/12/google-direction-library-for-android-th.html?m=1

รู้จัก Firebase Realtime Database ตั้งแต่ Zero จนเป็น Hero

Firebase Realtime Database เป็น NoSQL cloud database ที่เก็บข้อมูลในรูปแบบของ JSON และมีการ sync ข้อมูลแบบ realtime กับทุก devices ที่เชื่อมต่อแบบอัตโนมัติในเสี้ยววินาที รองรับการทำงานเมื่อ offline(ข้อมูลจะถูกเก็บไว้ใน local จนกระทั่งกลับมา online ก็จะทำการ sync ข้อมูลให้อัตโนมัติ) รวมถึงมี Security Rules ให้เราสามารถออกแบบเงื่อนไขการเข้าถึงข้อมูลทั้งการ read และ write ได้ดังใจ ทั้ง Android, iOS และ Web

วิดีโอแนะนำการทำงานของ Firebase Realtime Database

ในการพัฒนา Firebase Realtime Database ขอแยกออกเป็น 5 parts ดังนี้

  1. การ Set up Firebase และ Realtime Database SDK
  2. การเขียนข้อมูล
  3. การอ่านข้อมูล
  4. การเปิดใช้งานโหมด Offline
  5. Security & Rules

เมื่อพร้อมแล้ว…ก็เปิด Android Studio ขึ้นมา โดยจะสร้างโปรเจคใหม่ หรือจะใช้โปรเจคเดิมก็ได้

Part 1 การ Set up Firebase และ Realtime Database SDK

ถ้าสร้างโปรเจคใหม่ ให้ไปดูการ Set up Firebase ที่บทความนี้ก่อน

เมื่อ Set up Firebase เรียบร้อยแล้ว ก็ให้เพิ่ม Realtime Database SDK ใน build.gradle ของ app-level แล้วกด Sync ก็เป็นอันจบส่วนที่ 1 ละ

dependencies {
     compile 'com.google.firebase:firebase-database:10.0.1'
}

การเข้าถึงข้อมูลสำหรับ Firebase Realtime Database ทั้ง read และ write โดยปกติ เราจะต้องทำการ Authentication ผ่าน Firebase Authentication ซะก่อน แต่เพื่อให้เราสามารถเข้าใจบทความนี้ได้โดยไม่ต้องอ่าน Firebase Authentication เราจะมาทำให้มันเข้าถึงได้แบบ public กัน โดยให้เข้าไปที่ Firebase Console เข้าไปที่โปรเจค จากนั้นเลือกเมนู Database แล้วเลือก tab ที่ชื่อว่า RULES จะพบหน้าตาของประมาณนี้

Default rules in Firebase Realtime Database

ด้านขวามือจะมี simulator ให้ลองทดสอบ rules ที่เราสร้างขึ้น ทั้งแบบ public หรือแบบ authentication แล้วก็ดี ดังนั้น ลองกด RUN แบบ default rules ก่อนเลย ผลปรากฎว่าถ้าไม่ได้ authentication ก็จะไม่สามารถเข้าถึงข้อมูลได้นั่นเอง ดังรูป

Run default rules ด้วย simulator

เอาหละ ต่อไปเรามาปรับให้มันเป็น public กันดีกว่า โดยให้เปลี่ยน rules ตามนี้

Run public rules ด้วย simulator

เมื่อกด RUN ดูก็พบว่า ไม่ต้อง Authentication ก็สามารถเข้าถึงข้อมูลได้ละ เมื่อเสร็จแล้วก็กดปุ่ม PUBLISH ด้วย เป็นอันจบ Part 1


Part 2 การเขียนข้อมูล (Write)

เริ่มด้วยการประกาศตัวแปร DatabaseReference รับค่า Instance และอ้างถึง path ที่เราต้องการใน database

DatabaseReference mRootRef = FirebaseDatabase.getInstance().getReference();

จากนั้นก็อ้างอิงไปที่ path ที่เราต้องการจะจัดการข้อมูล ตัวอย่างคือ users และ messages

DatabaseReference mUsersRef = mRootRef.child("users");
DatabaseReference mMessagesRef = mRootRef.child("messages");

การเขียนข้อมูล (Write)

การ write, update หรือ delete ข้อมูลใน Firebase Realtime Database จะรองรับค่าหลายประเภททั้ง String, Long, Double, Boolean, Map<String, Object> และ List<Object> โดยการ write จะมีด้วยกัน 4 รูปแบบดังนี้

1. setValue() เป็นการ write หรือ update ข้อมูล ไปยัง path ที่เราอ้างถึงได้ เช่น users/<user-id>/<username>

mUsersRef.child("id-12345").setValue("Jirawatee");

เราสามารถดูผลลัพธ์แบบ realtime ได้ที่ Firebase Console โดยไปที่เมนู Database แล้วเลือก tab แรก คือ Data เราก็จะเห็นข้อมูลทั้งหมดแบบทุกลมหายใจละ

ตัวอย่างการ setValue()

2. push() เป็นการเพิ่มชุดของข้อมูล ในที่นี้ผมจะสร้าง model object ชื่อ FriendlyMessage ซึ่งจะบรรจุ text และ username ไว้ โดยการ push นั้น Firebase จะสร้าง unique key ของชุดข้อมูลนั้นๆ เพื่อใช้อ้างอิงต่อไปได้ เช่น messages/<message-id>/<data-model>

FriendlyMessage friendlyMessage = new FriendlyMessage("Hello World!", "Jirawatee");
mMessageRef.push().setValue(friendlyMessage);
ตัวอย่างการ push()

3. updateChildren() เป็นการ write หรือ update ข้อมูลบางส่วน(บาง key) ตาม path ที่เราอ้างถึง โดยไม่ต้องทำการ replace ข้อมูลทั้งชุด และสามารถทำพร้อมๆกันได้หลาย object

ตัวอย่างจะเป็นการสร้าง post ใหม่ขึ้นมา โดยจะ write ข้อมูลไป 2 ที่คือ
/user-messages/Jirawatee/$postid และ /messages/$postid

// push เป็นการ generate $postid ของ object ชื่อ posts ออกมาก่อนเพื่อใช้ใน // /user-posts/$userid/$postid 
String key = mMessagesRef.push().getKey();
HashMap<String, Object> postValues = new HashMap<>();
postValues.put("username", "Jirawatee");
postValues.put("text", "Hello World!");

Map<String, Object> childUpdates = new HashMap<>();
childUpdates.put("/messages/" + key, postValues);
childUpdates.put("/user-messages/Jirawatee/" + key, postValues);

rootRef.updateChildren(childUpdates);

สีที่แสดงใน console จะบอกสถานะแตกต่างกันดังนี้
สีเหลือง: แสดงการอัพเดท
สีเขียว: แสดงการเพิ่ม
สีแดง: แสดงการลบ
สีน้ำเงิน: แสดงการเคลื่อนย้าย

write ข้อมูลเข้าไปทั้ง 2 objects

ในกรณีที่ต้องการอัพเดทข้อมูลบางส่วน ก็สามารถทำได้พร้อมๆกันได้ โดยจะต้องรู้ username และ message-id เป็นตัวระบุในแต่ละ object

update ข้อมูลบางส่วนทั้ง 2 objects

4. runTransaction() เป็นการอัพเดทข้อมูล ที่มี concurrent เยอะๆ ที่อาจเกิดชนกัน เกิดข้อผิดพลาดได้ ตัวอย่างเช่น การกด like และกด unlike ที่โพสเดียวกัน เวลาเดียวกัน จะต้องมีการนับยอด like ตลอดเวลา ว่าช่วงเวลานั้นเป็นเท่าไร

postRef.runTransaction(new Transaction.Handler() {
     @Override
     public Transaction.Result doTransaction(MutableData mutable) {
        Post p = mutable.getValue(Post.class);
        if (p == null) {
           return Transaction.success(mutable);
        }

        if (p.stars.containsKey(getUid())) {
           // Unlike the post and remove self from likes
           p.starCount = p.starCount - 1;
           p.stars.remove(getUid());
        } else {
           // Like the post and add self to likes
           p.starCount = p.starCount + 1;
           p.stars.put(getUid(), true);
        }

        // Set value and report transaction success
        mutable.setValue(p);
        return Transaction.success(mutable);
      }

      @Override
      public void onComplete(DatabaseError databaseError, boolean b, DataSnapshot dataSnapshot) {
          if (databaseError != null) {
             databaseError.getMessage()
             Log.w(TAG, databaseError.getMessage());
          } else {
             Log.d(TAG, "Transaction successful");
          }
      }
});

การลบข้อมูล (Delete)

การลบข้อมูลนั้น ให้เราระบุ path ที่เราต้องการจะลบ จากนั้นก็เรียกคำสั่ง removeValue() ตัวอย่างเช่น ต้องการลบข้อความทั้งหมดใน object ชื่อ messages

mMessageRef.removeValue();

นอกจากนั้นเรายังสามารถลบข้อมูล ได้ด้วยการส่งค่า null ไปที่ setValue(null) และสามารถใช้ค่า null กับเทคนิค updateChildren() เพื่อลบข้อมูลหลายๆ object ได้ด้วย

// ลบแบบ setValue()
mMessageRef.setValue(null);
// ลบแบบ updateChildren();
childUpdates.put("/messages/", null);

Part 3 การอ่านข้อมูล (Read)

เริ่มด้วยการประกาศตัวแปร DatabaseReference รับค่า Instance และอ้างถึง path ที่เราต้องการใน database

DatabaseReference mRootRef = FirebaseDatabase.getInstance().getReference();

การอ่านข้อมูลใน Firebase Realtime Database จะมี 2 ประเภทแยกตาม Listener ดังนี้

1. ValueEventListener

จะอ่านข้อมูลตั้งแต่เริ่ม และ จะอ่านข้อมูลทุกครั้งที่มีการเปลี่ยนแปลงของข้อมูลทั้งหมดภายใต้ path ที่เราอ้างถึง วิธีการคือใช้ object ที่เราอ้างถึงมา addValueEventListener โดยจะมี callback 2 แบบ

  • onDataChange จะถูกเรียกตอนเริ่ม และถูกเรียกทุกครั้งที่ข้อมูลภายใต้ path ที่เราอ้างถึงมีการเปลี่ยนแปลง
  • onCancelled จะถูกเรียกเมื่อไม่สามารถอ่านข้อมูลจาก database ได้
mRootRef.addValueEventListener(new ValueEventListener() {
    @Override
    public void onDataChange(DataSnapshot dataSnapshot) {
        String value = dataSnapshot.getValue(String.class);
        mTextView.setText(value);
    }

    @Override
    public void onCancelled(DatabaseError error) {
        mTextView.setText("Failed: " + databaseError.getMessage());
    }
});

ข้อควรระวังของ ValueEventListener คือไม่ควรอ้างถึง root เนื่องจากมันจะคอยดูข้อมูลทั้งหมดของ database ซึ่งมันอาจมีขนาดใหญ่ และ เปลือง bandwidth โดยใช่เหตุ เราจึงควรใช้วิธีนี้แบบเจาะจงเฉพาะ หรืออ่านข้อมูลทั้งหมดครั้งแรก แล้ว removeEventListener() ออกไป

2. ChildEventListener

จะคอยรับข้อมูลจาก การเพิ่ม, การเปลี่ยนแปลง, การลบ และ การย้าย เฉพาะของ child ที่เราอ้างถึง วิธีการคือใช้ object ที่เราอ้างถึงมา addChildEventListener โดยจะมี callback 5 แบบ

  • onChildAdded() จะถูกเรียกเมื่อมีการเพิ่มชุดข้อมูลเข้ามาใน child
  • onChildChanged() จะถูกเรียกเมื่อข้อมูลใน child มีการเปลี่ยนแปลง
  • onChildRemoved() จะถูกเรียกเมื่อข้อมูลใน child ถูกลบ
  • onChildMoved() จะถูกเรียกเมื่อมีการเรียงลำดับของข้อมูลใน child เกิดขึ้น
  • onCancelled() จะถูกเรียกเมื่อโหลดข้อมูลจาก child ไม่สำเร็จ

ตัวอย่างการ addChildEventListener ไปกับการ comment ซึ่งเหมาะมากกับ RecyclerView

ChildEventListener childEventListener = new ChildEventListener() {
    @Override
    public void onChildAdded(DataSnapshot dataSnapshot, String previousChildName) {
       // A new comment has been added, add it to the displayed list
       Comment comment = dataSnapshot.getValue(Comment.class);
       // ...
    }

    @Override
    public void onChildChanged(DataSnapshot dataSnapshot, String previousChildName) {
        // A comment has changed, use the key
        // to determine if we are displaying this
        // comment and if so displayed the changed comment.
        Comment newComment = dataSnapshot.getValue(Comment.class);
        String commentKey = dataSnapshot.getKey();
        // ...
    }

    @Override
    public void onChildRemoved(DataSnapshot dataSnapshot) {
        // A comment has changed, use the key
        // to determine if we are displaying this
        // comment and if so remove it.
        String commentKey = dataSnapshot.getKey();
        // ...
    }

    @Override
    public void onChildMoved(DataSnapshot dataSnapshot, String previousChildName) {
        // A comment has changed position,
        // use the key to determine if we are
        // displaying this comment and if so move it.
        Comment movedComment = dataSnapshot.getValue(Comment.class);
        String commentKey = dataSnapshot.getKey();
        // ...
    }

    @Override
    public void onCancelled(DatabaseError databaseError) {
        Toast.makeText(mContext, "Failed to load comments.",
                Toast.LENGTH_SHORT).show();
    }
};
ref.addChildEventListener(childEventListener);

การเพิ่ม listener เข้าไปหลายตัว นั่นก็แปลว่าจะต้องมีการ call เกิดขึ้นมากมายตามแต่ event ซึ่ง หากเราไม่ได้ใช้ หรือออกจากหน้าดังกล่าว ก็ควรจะถอด listener เหล่านั้นออกไปด้วย เพื่อการใช้ bandwidth แบบคุ้มค่ามากที่สุดโดยสามารถถอดออกได้ด้วยคำสั่ง removeEventListener()

@Override
protected void onStop() {
   super.onStop();
   if (mValueEventListener != null) {
      mRootRef.removeEventListener(mValueEventListener);
   }
}

การอ่านข้อมูลเพียงครั้งเดียว

บางครั้งเราอาจต้องการอ่านข้อมูลแค่ครั้งเดียวและก็ไม่สนใจมันอีก Firebase ได้เตรียม addListenerForSingleValueEvent() ที่เมื่อได้ callback แล้วก็จะ remove listener ทิ้งอัตโนมัติ ตัวอย่างเช่น ดึงข้อมูลผู้ใช้ก่อนทำการโพส ว่าเขาผ่านการ sign-in มาหรือยัง หรือยังขาดข้อมูลอะไรที่ต้องการ หากครบถ้วนก็โพสได้ แต่หากไม่ครบอาจพาไปหน้า sign-in หรือพาไปหน้ากรอกข้อมูลให้ครบถ้วน

mDatabase.child("users").child(userId).addListenerForSingleValueEvent(new ValueEventListener() {
   @Override
   public void onDataChange(DataSnapshot dataSnapshot) {
      User user = dataSnapshot.getValue(User.class);
      if (user == null) {
         Toast.makeText(NewPostActivity.this, "Error: could not fetch user.", Toast.LENGTH_LONG).show();
      } else {
         writeNewPost(userId, user.username, title, body);
      }
      finish();
   }

   @Override
   public void onCancelled(DatabaseError databaseError) {
      Log.e(TAG, databaseError.getMessage());
   }
});

การเรียงลำดับและการกรองข้อมูล (Sorting and Filtering)

การ query ข้อมูล ของ Firebase Realtime Database นั้น รองรับการ sort และ filter ได้ อย่างก็ดีการ sort และ filter สามารถทำให้การ query นั้นช้าได้ ทางที่ดีควรศึกษาเรื่องการทำ index (.indexOn) ไปด้วยจะทำให้การ query นั้นมีประสิทธิภาพมากขึ้น

การ Sort ข้อมูล มี 3 รูปแบบ ดังนี้ (Ordering Function)

  • orderByChild() เป็นการเรียงลำดับ value ของ child key ที่ถูกเลือก (การใช้งานคล้าย WHERE ใน SQL)
  • orderByKey() เป็นการเรียงลำดับ child key (ใช้เรียง PK เหมาะกับแสดงข้อมูลแบบมี limit หรือแสดง pagination)
  • orderByValue() เป็นการเรียงลำดับ child value (เหมาะกับการเรียงค่าตัวเลข)
// ตัวอย่างการเรียงลำดับโพสของฉันที่ได้รับคะแนนโหวตมากที่สุด
dbReference.child("user-posts").child(getUid()).orderByChild("starCount");

การ Filter ข้อมูล มี 5 รูปแบบ ดังนี้ (Querying Function)

  • limitToFirst() การระบุจำนวน item ซึ่งจะเรียงลำดับจากแถวแรก
  • limitToLast() การระบุจำนวน item ซึ่งจะเรียงลำดับจากแถวสุดท้าย
  • startAt() จะดึงจำนวน item ที่มากกว่า หรือ เท่ากับ ที่ระบุ key หรือ value โดยขึ้นอยู่กับการ order-by
  • endAt() จะดึงจำนวน item ที่น้อยกว่า หรือ เท่ากับ ที่ระบุใน key หรือ value โดยขึ้นอยู่กับการ order-by
  • equalTo() จะดึงจำนวน item ที่เท่ากับที่ระบุใน key หรือ value โดยขึ้นอยู่กับการ order-by
// ตัวอย่างการดึงข้อมูลโพส 100 อันล่าสุด
databaseReference.child("posts").limitToFirst(100);

Part 4 การเปิดใช้งานโหมด offline

ลักษณะการทำงาน offline (Persistence Behavior)

Firebase Realtime Database มีการจัดการเรื่องการเชื่อมต่อให้แล้ว เมื่อไม่สามารถเชื่อมต่ออินเตอร์เน็ต หรืออยู่ในโหมด offline เรายังสามารถใช้งานแอพได้ โดยตัว Firebase จะทำการ cache ข้อมูลไว้ทุกการกระทำ และจะทำการ sync ข้อมูลให้อัตโนมัติเมื่อเรากลับเข้าสู่โหมด online แม้ว่าเราจะปิดแล้วเปิดแอพใหม่ก็ตาม มันยอดมากเลย แค่ประกาศใช้งานง่ายๆเพียงบรรทัดเดียว

FirebaseDatabase.getInstance().setPersistenceEnabled(true);

*******************************************************************
ปัญหาที่หลายคนประกาศ 
.setPersistenceEnabled แล้วเมื่อออกและเข้ามาใหม่ปรากฏว่า crash โดยแสดง error ประมาณนี้

com.google.firebase.database.DatabaseException: Calls to setPersistenceEnabled() must be made before any other usage of FirebaseDatabase instance

ผู้เขียนแนะนำให้แก้ปัญหานี้โดย
1. สร้าง class ที่ extends Application แล้วไปประกาศ
setPersistenceEnabled(true)ที่นั่น

class MyApp extends android.app.Application 

@Override
public void onCreate() {
    super.onCreate();
    FirebaseDatabase.getInstance().setPersistenceEnabled(true);
}

2. ใน AndroidManifest ให้ไปประกาศ class ในข้อ 1 ใน <application> ซะ

android:name="com.example.MyApp"

ผู้เขียนทดสอบแล้ว ประกาศที่เดียว จบ
*******************************************************************

แอบ sync ข้อมูลแบบลับๆ (Keeping Data Fresh)

Firebase Realtime Database จะ sync และเก็บข้อมูลใน local ของ client เฉพาะข้อมูลที่ตัว listener ทำงานอยู่ แต่เราสามารถจะ sync ข้อมูลส่วนอื่นที่ยังไม่ได้ active ได้ โดยให้อ้างถึง path ที่ต้องการ sync ตัวอย่างเช่น

DatabaseReference scoresRef = FirebaseDatabase.getInstance().getReference("scores");
scoresRef.keepSynced(true);

และโดยทั่วไปเราจะเก็บข้อมูลลง cache ได้ไม่เกิน 10MB แต่หากแอพมีข้อมูลที่ต้องการ cache มากกว่านั้น Firebase Realtime Database จะ purge cache ออก แต่ข้อมูลที่เก็บแบบ keepSynced จะไม่ถูก purge นะเออ

การ Query ข้อมูลในขณะที่ Offline

เมื่อแอพ sync ข้อมูลตอน online มาไว้ในเครื่องแล้ว และเมื่อแอพเข้าสู่โหมด offline เราก็ยังสามารถ query ข้อมูลที่ sync มาแล้วได้ ตัวอย่างเช่น

// ตอน online ได้ผลลัพธ์ 4 แถว จากการ query
scoresRef.orderByValue().limitToLast(4)
// เมื่อ offline หากต้องการ query ผลลัพธ์ 2 แถว ก็ทำได้
scoresRef.orderByValue().limitToLast(2)

การ run ตัว Transactions ขณะ Offline

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


Part 5 Security & Rules

Firebase Realtime Database Rules เป็นการกำหนดการเข้าถึง database ทั้งการ read, write และการทำ index โดย rules ทั้งหมดจะอยู่บน Firebase server แล้วเราสามารถปรับเปลี่ยนและมีผลทันทีได้ตลอดเวลา ซึ่งมี syntax เป็น Javascript-like โดยจะแบ่ง rules ออกเป็น 4 ประเภทดังนี้

  1. .read คือ การอนุญาตให้อ่าน
  2. .write คือ การอนุญาตให้เขียน
  3. .validate คือ การตรวจสอบข้อมูลที่เข้ามา
  4. .indexOn คือ การทำ index เพื่อความรวดเร็วในการเรียงลำดับ และการ query

โดยการกำหนด rules ต่างๆให้เข้าไปที่ Firebase Console เลือกโปรเจค จากนั้นเลือกเมนู Database แล้วเลือก tab ชื่อ RULES

สำหรับรายละเอียด Security & Rules นั้นมีมากมาย เช่น การกำหนด rules สำหรับการ read และ write ของแต่ละ path ที่ยึดตามโครงสร้างของ database, การกำหนด rules สำหรับการ validate การเพิ่มข้อมูล หรือ อัพเดทข้อมูล และการให้เข้าถึงข้อมูลตัวเองของผู้ใช้คนนั้นๆ เป็นต้น แนะนำให้ไปศึกษาเพิ่มเติมตามลิงค์นี้ครับ https://firebase.google.com/docs/database/security/ รับรองว่ามันไม่ยาก แค่มันไม่ง่ายแค่นั้นเอง


เคล็ดไม่ลับ

รับ Callback หลังจาก setValue() หรือ updateChildren()

เราสามารถ add completion callback ได้ทั้ง setValue() และ updateChildren() ได้ เผื่อใครอยากจะ handle กรณีที่ไม่สำเร็จ ดังตัวอย่างด้านล่าง

mUsersRef.child("id-12345").setValue("Jirawatee", new DatabaseReference.CompletionListener() {
   @Override
   public void onComplete(DatabaseError databaseError, DatabaseReference databaseReference) {
      if (databaseError != null) {
         Log.w(TAG, "setValue() failure");
      } else {
         Log.d(TAG, "setValue() successful");
      }
   }
});

การ Minify

หากทำการ minify กับโปรเจค ที่มีการใช้งาน model object กับฟังก์ชัน DataSnapshot.getValue(Class) หรือ DatabaseReference.setValue(Object) เราจำเป็นต้องเพิ่ม code ด้านล่างนี้ในไฟล์ proguard-rules.pro ด้วยนะ

# Add this global rule
-keepattributes Signature

# This rule will properly ProGuard all the model classes in
# the package com.yourcompany.models. Modify to fit the structure
# of your app.
-keepclassmembers class com.yourcompany.models.** {
  *;
}

Best practice ในการออกแบบโครงสร้างของข้อมูล

เราควรหลีกเลี่ยงการซ้อนกันของ data เพราะ Firebase Realtime Database อนุญาตให้เราออกแบบ data ซ้อนกันได้ไม่เกิน 32 ชั้น เพราะหากเรา fetch ข้อมูลจาก database ที่มีข้อมูลซ้อนกันเยอะ เราอาจต้อง fetch ข้อมูลจำนวนมาก ทำให้เปลือง bandwidth ได้ ควรออกแบบโครงสร้างแบบ flat เพื่อขนาด data ที่เล็กและเร็วในการ fetch ซึ่งประสิทธิภาพที่สูงกว่านั้น เราจะต้องแลกด้วยการไม่ทำ normalize ข้อมูล หรือเรียกว่า denormalize นั่นเอง ขอแค่อย่าลืมว่า เมื่อมีการอัพเดทหรือลบข้อมูลก็ต้องทำให้ครบทุกที่ที่เราเพิ่มข้อมูลเหล่านั้นเข้าไปด้วย

จากตัวอย่างเรามีการ add key มากกว่า 1 ที่ โดยสามารถแยกการ query ออกเป็น 3 มุมมองดังนี้

  1. Query ข้อมูลของ user ทั้งหมดจาก users
  2. Query ข้อมูลของ content ทั้งหมดจาก posts
  3. Query ข้อมูล content ของ user แต่ละคนจาก user-posts

ทั้ง 3 แบบเราสามารถ query ข้อมูลได้อย่างมีอย่างรวดเร็ว และยังสามารถทำ offset และ limit ของ data ได้ด้วย

ประเมินค่าใช้จ่าย

Firebase Realtime Database นั้นเป็นบริการที่ฟรีแบบจำกัด โดยจะมีเรื่องให้พิจารณาด้วยกัน 4 เรื่องคือ

  1. จำนวนการใช้งานพร้อมๆกัน (Connection)
  2. พื้นที่ที่เก็บข้อมูล (Storage)
  3. ขนาดของการส่งรับข้อมูล (Bandwidth)
  4. การสำรองข้อมูล (Backup)

โดยสถิติการใช้งานทั้งหมดเราสามารถเช็คได้ที่ Firebase Console โดยเข้าไปที่เมนู Database แล้วเลือก tab ชื่อ USAGE

และในหน้า Pricing ของ Firebase เราสามารถจะประมาณการใช้งานได้ว่าใช้เท่าไรคิดเงินเท่าไร ตัวอย่าง ผมจะจำลองการใช้งานของแพ็คเกจ SPARK ซึ่งเป็นแพ็คเกจฟรี โดย storage 1 GB เก็บข้อมูลได้ประมาณ 20 ล้านข้อความ และ bandwidth 10 GB จะ read และ write ได้ประมาณ 200 ล้านข้อความ เรียกได้ว่าแค่แพ็คเกจฟรี ก็ใช้งานได้เยอะเลยทีเดียว

หากแอพของเรามีข้อมูลเยอะ transaction เยอะ ก็เลือกใช้ตามแพ็คที่มีค่าใช้จ่ายที่เหมาะสมต่อไป ดูจากราคาแล้วเรียกว่าคุ้มค่ากับความเสถียร, ความปลอดภัย และเวลาในการพัฒนาแน่นอน

ตัวอย่างพร้อมแล้วใน GitHub

ตัวอย่างมีทั้งแบบการใช้งานแบบ authentication กับ public เลยครับ โดยตัว public จะเป็นตัว basic และตัว authentication จะ advance หน่อย มีตัวอย่างทั้งแอพ quickstart ของ Firebase ที่เอามา modify และตัวอย่าง chat app ด้วย โหลดเลย


ทิ้งไว้กลางทาง…เอ้ย ทิ้งท้าย

เป็นอย่างไรบ้างครับกับ Firebase Realtime Database ดูท่าทางน่าจะทำให้ชีวิตหลายคนดีขึ้นมาเลยทีเดียว ก่อนจะเอาขึ้น Production อย่าลืมไปตั้ง Security Rules ให้ต้อง Authentication ก่อนเข้าถึง database นะครับ ไม่งั้นเดี๋ยวคนอื่นแอบมาเขียน database เราไม่รู้ด้วย สำหรับบทความนี้ก็ทิ้งห่างจากบทความที่แล้วไปครึ่งเดือนเท่านั้นเอง ไม่นานเล้ยยย(เสียงสูง) เอาเป็นว่าบทความต่อไปก็จะเป็นเรื่องของ Firebase Storage ซึ่งจะไม่ทิ้งช่วงนานแน่นอน เราจะทำตามสัญญา ขอเวลาอีกไม่นาน…สำหรับวันนี้ต้องลาไปก่อน ราตรีสวัสดิ์พี่น้องชาวไทย

ref:https://developers.ascendcorp.com/%E0%B8%A3%E0%B8%B9%E0%B9%89%E0%B8%88%E0%B8%B1%E0%B8%81-firebase-realtime-database-%E0%B8%95%E0%B8%B1%E0%B9%89%E0%B8%87%E0%B9%81%E0%B8%95%E0%B9%88-zero-%E0%B8%88%E0%B8%99%E0%B9%80%E0%B8%9B%E0%B9%87%E0%B8%99-hero-5d09210e6fd6

การนำทาง หรือ การค้นหาเส้นทาง บน แอนดรอยด์

การนำทาง หรือ การค้นหาเส้นทาง บน แอนดรอยด์ เป้นตัวอย่างการ route direction ระหว่าง จุด marker บน map Android ก่อนอื่นต้องขอ ชอบคุณ ตุณเอก https://www.facebook.com/Akexorcist/ ที่สร้าง library ดีๆ ที่ทำให้การ Routing map ง่ายมาก มาสเตอร์ ขออนุญาติ เผยแพร์ Library ของคุณเอก นะครับ

เริ่มต้นจากการที่เราจะต้องติดตั่ง Library ก่อน ไปที่ build.gradle ก่อน

และ เพิ่ม

และ compile ส่ิงนี่เข้าไป compile ‘com.akexorcist:google.directionlibrary:1.0.5’

กลับมาที่ Class ใ้ห้ Implement DirectionCallback

9

ตัว Studio จะขอ Impliment Method 2 ตัวนี่

ทีี method onDirectionSuccesss และ onDirectionFailure ให้เพิ่มคำสังแบบนี่เข้าไป

สำคัญที่สุดเราต้องไปสร้าง ServerKey ด้วยนะครับ

ref:http://androidthai.in.th/index.php/android-article/16-drawing-driving-route-directions-between-two-locations-using-google-directions-in-google-map-android-api-v2