เนื่องจากได้มีโอกาสไปร่วมแจมงาน Firebase Dev Day ใน Session ที่ชื่อว่า Code Battle ซึ่งเป็นการโชว์โค้ดสดเพื่อสร้างแอปฯตัวหนึ่งขึ้นมาโดยใช้ Firebase ที่ทำงานได้เหมือนกันไม่ว่าจะอยู่บน iOS, Android และ Web บวกกับความฮาและความสนุกที่เกิดขึ้นด้วย
แต่ความสนุกของ Code Battle นั้นยังไม่จบเพียงเท่านี้ เพราะเจ้าของบล็อกจะหยิบเนื้อหาจาก Code Battle ของฝั่ง Android มาเล่าแบบ Deep Dive กันต่อว่าโค้ดที่ใช้ในช่วง Code Battle นั้นมีอะไรบ้าง
ที่มาของ Code Battle
ตอนแรกก็จะเป็น Session ธรรมดาๆที่มีเนื้อหาเกี่ยวกับ Firebase บน Android น่ะแหละ แต่เนื้อหาของฝั่ง Android ก็มีค่อนข้างเยอะแล้ว (จากหลายๆบทความที่นักพัฒนาช่วยเขียนกัน) พี่ตี๋ +Jirawat Karanwittayakarn ที่เคยดูวีดีโอของ Firebase รู้สึกประทับใจไอเดียของ Code Battle ที่ใช้ในงานนั้นมากๆ จึงเสนอว่าทำให้ Code Battle ใน Session สุดท้ายนี้ไปเลย
การทำ Code Battle ก็ดีตรงที่ทุกๆคนจะได้เห็นไปพร้อมๆกันนี่แหละว่าแต่ละ Platform ทำอะไรยังไงบ้าง ถ้าจะใช้ Firebase ตัวไหน ต้องเรียกใช้งานยังไง ซึ่งจะช่วยให้เห็นภาพรวมของทุก Platform ได้ง่ายมาก
แต่ติดอย่างเดียวคือ...จะทำยังไงให้ใช้เวลาน้อยที่สุด
ถึงจะเตรียมตัวมาดีแค่ไหน แต่การพิมพ์โค้ดทั้งหมดก็ใช้เวลาประมาณ 1 ชั่วโมงเป็นอย่างน้อย แถมการทำพร้อมๆกันก็ไม่ค่อยโอเคซักเท่าไร เพราะมีบางขั้นตอนที่ต้องผลัดกันอธิบาย จึงทำให้ใช้เวลานานขึ้นไปอีก
ดังนั้นเพื่อไม่ให้ Session นี้ใช้เวลานานเกินจำเป็นจึงใช้วิธีเตรียมโค้ดไว้เป็น Snippet กัน (Code Battle ในงาน Firebase ก็ใช้วิธีแบบนี้เช่นกัน)
แต่นั่นก็จะทำให้รู้สึกว่ามันไม่ค่อยลงลึกในรายละเอียดซักเท่าไร เป็นเหมือนการ Demo ให้ดูคร่าวๆเท่านั้น ดังนั้นบทความนี้ก็จะพาผู้ที่หลงเข้ามาอ่านมาลงลึกกับโค้ดของฝั่ง Android ที่ใช้ใน Code Battle จากงาน Firebase Dev Day กันฮะ
• ใช้ Firebase Authentication ในส่วนของ Log in
• Login ด้วย Google+
• สามารถ Log out ได้
• ใช้ Firebase Realtime Database ในส่วนของ Chat
• ใช้ Firebase Storage ในการเก็บไฟล์ภาพและแสดงผลใน Chat
• สามารถ Log out
• ไม่ต้องเก็บรายละเอียดโค้ดทั้งหมดเนื่องจากต้องการการทำงานของโค้ดบางส่วนเท่านั้น
• ใช้เวลาไม่นานจนเกินไป
• get_started : โค้ดแบบเริ่มต้นเพื่อให้ผู้ที่หลงเข้ามาอ่านได้ลองทำตามไปพร้อมๆกับบทความนี้
• complete : โค้ดแบบเสร็จแล้ว
ซึ่งใน get_started ก็จะมีโค้ดในส่วนอื่นๆเตรียมไว้ให้แล้ว เหลือแค่ส่วนที่เป็นของ Firebase ทั้งหมดที่เจ้าของบล็อกทำเป็น TODO ตามลำดับไว้ให้ และจะใช้อ้างอิงในบทความด้วยเช่นกัน (จะได้รู้ว่ากำลังหมายถึงตรงไหนของโค้ด)
สำหรับ build.gradle ที่อยู่ใน Root Project นั้นจะมีการเพิ่ม Repository เข้ามาอีกนิดหน่อย เนื่องจากมี Library บางตัวไม่ได้อยู่ใน JCenter แล้วก็อย่าลืม Classpath ของ Google Play Services เด็ดขาดล่ะ
หมายเหตุ - Module ที่ชื่อว่า app จะใช้คำว่า "Module หลัก" ส่วน Module ที่ชื่อว่า library จะใช้คำสั่ง "Library"
ในนี้ก็จะมี BaseActivity ที่เตรียมคำสั่งไว้ให้เรียกใช้งานง่ายขึ้น ซึ่งเตรียมคำสั่งสำหรับ Loading Dialog กับ Snackbar เอาไว้ เพื่อลดความรกของโค้ดใน Module หลัก
• showLoading : แสดง Loading Dialog เพื่อให้รู้ว่ากำลังโหลดข้อมูลอยู่
• hideLoading : ซ่อน Loading Dialog
• isLoadingShowing : เช็คว่า Loading Dialog กำลังแสดงอยู่หรือไม่
การทำ Code Battle ก็ดีตรงที่ทุกๆคนจะได้เห็นไปพร้อมๆกันนี่แหละว่าแต่ละ Platform ทำอะไรยังไงบ้าง ถ้าจะใช้ Firebase ตัวไหน ต้องเรียกใช้งานยังไง ซึ่งจะช่วยให้เห็นภาพรวมของทุก Platform ได้ง่ายมาก
แต่ติดอย่างเดียวคือ...จะทำยังไงให้ใช้เวลาน้อยที่สุด
ถึงจะเตรียมตัวมาดีแค่ไหน แต่การพิมพ์โค้ดทั้งหมดก็ใช้เวลาประมาณ 1 ชั่วโมงเป็นอย่างน้อย แถมการทำพร้อมๆกันก็ไม่ค่อยโอเคซักเท่าไร เพราะมีบางขั้นตอนที่ต้องผลัดกันอธิบาย จึงทำให้ใช้เวลานานขึ้นไปอีก
ดังนั้นเพื่อไม่ให้ Session นี้ใช้เวลานานเกินจำเป็นจึงใช้วิธีเตรียมโค้ดไว้เป็น Snippet กัน (Code Battle ในงาน Firebase ก็ใช้วิธีแบบนี้เช่นกัน)
แต่นั่นก็จะทำให้รู้สึกว่ามันไม่ค่อยลงลึกในรายละเอียดซักเท่าไร เป็นเหมือนการ Demo ให้ดูคร่าวๆเท่านั้น ดังนั้นบทความนี้ก็จะพาผู้ที่หลงเข้ามาอ่านมาลงลึกกับโค้ดของฝั่ง Android ที่ใช้ใน Code Battle จากงาน Firebase Dev Day กันฮะ
ต้องการแสดงอะไรบ้างใน Code Battle
• สร้าง Chat app โดยใช้ Firebase• ใช้ Firebase Authentication ในส่วนของ Log in
• Login ด้วย Google+
• สามารถ Log out ได้
• ใช้ Firebase Realtime Database ในส่วนของ Chat
• ใช้ Firebase Storage ในการเก็บไฟล์ภาพและแสดงผลใน Chat
• สามารถ Log out
• ไม่ต้องเก็บรายละเอียดโค้ดทั้งหมดเนื่องจากต้องการการทำงานของโค้ดบางส่วนเท่านั้น
• ใช้เวลาไม่นานจนเกินไป
เตรียมตัวก่อนจะอ่านต่อ
บทความนี้ตั้งใจว่าจะอธิบายเกี่ยวกับโค้ดที่ใช้ในโปรเจค Code Battle ซึ่งเจ้าของบล็อกได้เตรียมไว้ให้เรียบร้อยแล้วใน CodeBattle-Android [GitHub] โดยแบ่งออกเป็น 2 Branch ดังนี้• get_started : โค้ดแบบเริ่มต้นเพื่อให้ผู้ที่หลงเข้ามาอ่านได้ลองทำตามไปพร้อมๆกับบทความนี้
• complete : โค้ดแบบเสร็จแล้ว
ซึ่งใน get_started ก็จะมีโค้ดในส่วนอื่นๆเตรียมไว้ให้แล้ว เหลือแค่ส่วนที่เป็นของ Firebase ทั้งหมดที่เจ้าของบล็อกทำเป็น TODO ตามลำดับไว้ให้ และจะใช้อ้างอิงในบทความด้วยเช่นกัน (จะได้รู้ว่ากำลังหมายถึงตรงไหนของโค้ด)
ในโปรเจคมีอะไรบ้าง ก่อนจะเริ่มส่วนของ Firebase
แอปฯที่ใช้ใน Code Battle จะมีสองหน้าด้วยกัน คือหน้า Log in (LoginActivity) และห้อง Chat (ChatActivity) โดยที่ได้เตรียม UI ไว้ให้เรียบร้อยแล้ว (แต่ไม่ถึงกับ Responsive นะจ๊ะ)สำหรับ build.gradle ที่อยู่ใน Root Project นั้นจะมีการเพิ่ม Repository เข้ามาอีกนิดหน่อย เนื่องจากมี Library บางตัวไม่ได้อยู่ใน JCenter แล้วก็อย่าลืม Classpath ของ Google Play Services เด็ดขาดล่ะ
Library Module
เนื่องจากมันมีโค้ดบางอย่างที่ไ่มได้เกี่ยวข้องกับ Firebase โดยตรง เจ้าของบล็อกจึงแยกโค้ดส่วนนั้นออกมาให้เป็น Library ไปซะ สังเกตได้จาก Module ที่ชื่อว่า library ที่อยู่ในโปรเจคตัวนี้หมายเหตุ - Module ที่ชื่อว่า app จะใช้คำว่า "Module หลัก" ส่วน Module ที่ชื่อว่า library จะใช้คำสั่ง "Library"
ในนี้ก็จะมี BaseActivity ที่เตรียมคำสั่งไว้ให้เรียกใช้งานง่ายขึ้น ซึ่งเตรียมคำสั่งสำหรับ Loading Dialog กับ Snackbar เอาไว้ เพื่อลดความรกของโค้ดใน Module หลัก
• showLoading : แสดง Loading Dialog เพื่อให้รู้ว่ากำลังโหลดข้อมูลอยู่
• hideLoading : ซ่อน Loading Dialog
• isLoadingShowing : เช็คว่า Loading Dialog กำลังแสดงอยู่หรือไม่
• showAlert : แสดงข้อความแจ้งเตือนผ่าน Snackbar
ดังนั้นใน LoginActivity และ ChatActivity ก็จะสืบทอดจาก BaseActivity อีกทีหนึ่ง เพื่อจะได้เรียกใช้คำสั่งสำหรับ Loading Dialog กับ Snackbar ง่ายๆ โดยการทำ Loading Dialog ก็จะเป็น Dialog แบบ Fragment Dialog ที่มีคลาสที่ชื่อว่า QuickDialog คอยควบคุมการทำงานอยู่เบื้องหลัง โดยใช้ Library ที่ชื่อว่า SmoothProgressBar เพื่อทำ Loading เป็นวงกลมหมุนติ๊วๆ
Dependency ของ Library ต่างๆที่ไม่เกี่ยวข้องกับ Firebaes โดยตรงก็จะมากองรวมกันอยู่ใน Library ตัวนี้ด้วยเช่นกัน
Library หลายๆตัวที่ใส่ไว้ในนี้จะถูกเรียกใช้ใน Module หลักด้วย ซึ่งเดี๋ยวเจ้าของบล็อกจะบอกให้อีกทีว่าตัวไหนใช้ตรงไหนของแอปฯ
Internet Permission ก็ถูกใส่ไว้ให้เรียบร้อยแล้วใน AndroidManifest.xml ของ Module ตัวนี้ จะได้ไม่ต้องไปนั่งใส่เองใน Module หลัก
ส่วนเรื่องสีที่ใช้ ปกติเจ้าของบล็อกจะมี Palette สีที่ใช้ทำงานส่วนตัวอยู่แล้ว ดังนั้นใน Library ตัวนี้ก็จะมี colors.xml ที่เต็มไปด้วยสีที่เจ้าของบล็อกมักจะใช้ แล้วเพิ่มสีที่เป็นธีมของงาน Code Battle เข้าไป อีกทีหนึ่ง
โดยสีเหล่านี้ก็จะถูกเรียกใช้จากใน Module หลักและ Library (จะไม่มีการใส่ค่าสีแบบดิบๆลงในโค้ดโดยตรง)
ขนาดหรือ Dimension พื้นฐานอย่าง Margin หรือ Font Size ก็จะเตรียมไว้ใน dimens.xml ด้วยเช่นกัน โดยจะมีหลายๆขนาดเพื่อให้ยืดหยุ่นกับการเรียกไปใช้งาน แบ่งเป็น Extra Extra Small, Extra Small, Small, Medium, Large, Extra Large, Extra Extra Large และ Extra Extra Extra Large
เพื่อไม่ให้โค้ดใน XML รก เจ้าของบล็อกก็จะมี Style ของ View พื้นฐานเตรียมไว้อยู่เช่นกัน โดยจะอยู่ใน styles.xml ของ Library นี่แหละ ก็จะมี Style ของ Text และ Button ในรูปแบบต่างๆเตรียมไว้ เพื่อให้เรียกใช้งานใน Layout XML ได้เลย จะได้ไม่ต้องมานั่งพิมพ์ Attribute ทีละตัวให้เปลืองบรรทัด
ในบางครั้ง Button ก็ไม่ต้องการพื้นหลัง แต่ก็ควรมี Selector ด้วย ดังนั้นก็จะมี Style ที่ชื่อว่า Borderless ของ Button ให้ด้วย ซึ่งสร้างจาก Shape XML นี่แหละ และปุ่มที่ใช้ใน Code Battle นี้ ก็ไม่ต้องการพื้นหลังด้วยเช่นกัน รวมไปถึง Shadow Gradient ที่ทำเป็น Shape XML ไว้ให้เรียกใช้งานได้เลย
มี Activity อยู่สองตัวคือ LoginActivity กับ ChatActivity โดยที่ ChatActivity นั้นจะมี Adapter และ View Holder ด้วย เพราะแสดงข้อความบน Recycler View รวมไปถึง Model ที่ชื่อว่า Message เอาไว้เก็บข้อมูลที่อยู่บน Firebase Realtime Database
ส่วน Dependency ที่เพิ่มเข้ามาก็จะเป็นของ Firebase และ Google Play Services เท่านั้น
ในหน้า Log in ก็จะมีแค่ปุ่ม Log in เท่านั้นที่จะต้องมีการเขียนโค้ดเพิ่ม แต่ปุ่มที่ว่านี้ก็ไม่ได้สร้างขึ้นมาเองนะ ใช้ SignInButton ซึ่งเป็น View สำเร็จรูปจาก Google Play Services Auth
ทีนี้ข้ามมาดูที่ไฟล์ LoginActivity.java กันต่อเลย
เนื่องจากเจ้าของบล็อกไม่ชอบยัดโค้ดต่างๆลงใน onCreate ตูมเดียวเลย เดี๋ยวมันรก ดังนั้นก็จะแยกเป็น Method ยิบย่อยตามหน้าที่แบบนี้
เมื่อดูเฉพาะโค้ดของปุ่ม Log in จะมีแค่นี้
ผู้ที่หลงเข้ามาอ่านอาจจะรู้สึกแปลกๆตรง onSignInClick() ซึ่งเจ้าของบล็อกเขียนไว้แบบนี้
ที่เขียนแบบนี้ก็เพราะว่าถ้าใช้ Lambda Expression หรือ Retrolambda แล้วโค้ดมันจะกระชับมากๆ
แต่ในโปรเจคก็ไม่ได้ใส่ Retrolambda ไว้ให้ เพราะเผื่อว่าผู้ที่หลงเข้ามาอ่านบางคนจะไม่เข้าใจโค้ดในส่วนนี้ จึงใส่เป็นโค้ดแบบปกติไว้ให้แทนครับ
ซึ่งจะประกอบไปด้วย Edit Text, Button และ Recycler View ซึ่งตรงนี้ไม่น่าจะต้องอธิบายอะไรมากนัก เพราะเป็น View พื้นฐานที่นักพัฒนาแอนดรอยด์แทบทุกคนได้ใช้งานกันอยู่แล้ว
สำหรับการแสดง Chat ด้วย Recycler View นั้นจะต้องสร้าง Item View สองแบบด้วยกันเพื่อแสดงข้อความคนละฝั่ง ซึ่งใน Code Battle ตกลงกันว่าจะให้ข้อความคนอื่นๆอยู่ฝั่งซ้าย และข้อความของตัวเองอยู่ฝั่งขวา
ดังนั้นเจ้าของบล็อกจึงสร้าง Layout สำหรับ Item View ทั้งสองแบบเป็นไฟล์ที่ชื่อว่า view_other_message_item.xml กับ view_your_message_item.xml
Layout ทั้งสองนั้นแทบจะเหมือนกันทั้งหมด ต่างกันแค่เรื่องชิดซ้าย/ขวาเท่านั้นเอง ดังนั้นเจ้าของบล็อกจึงกำหนด ID ของ View ให้เหมือนกันด้วยเลย
พอลองดูภาพตัวอย่างของ Code Battle ก็จะเห็นว่าตรงรูปโปรไฟล์นั้นเป็นวงกลม ส่วนข้อความก็จะมีพื้นหลังเป็นกล่องคำพูด
ทำยังไงดีล่ะ?
คำตอบก็คือใช้ Library นั่นแหละฮะ จะได้ไม่ต้องเสียเวลา
สำหรับรูปโปรไฟล์วงกลมจะใช้ CircleImageView จะได้ไม่ต้องมานั่งทำเอง (ถึงแม้ว่า Glide จะทำได้ก็ตาม) ส่วนกล่องข้อความก็ใช้ ChatMessageView ที่สามารถปรับตำแหน่งของลูกศรคำพูดได้ตามใจชอบ (ยืดหยุ่นต่อการนำไปใช้งานดี)
และเนื่องจาก Item View ทั้งสองแบบนั้นใช้ชื่อ ID เหมือนๆกัน และทำหน้าที่เหมือนๆกัน จึงสามารถสร้าง View Holder แค่ตัวเดียวเพื่อใช้กับ Item View ทั้งคู่ได้เลย เย้เย
ก่อนจะไปดูคลาส Adapter ขอพากลับมาดูที่คลาส Model ที่ชื่อว่า Message สำหรับรับข้อมูลจาก Firebase Realtime Database ก่อน ซึ่งเจ้าของบล็อกและคนอื่นๆได้ตกลงกันว่าจะเก็บข้อมูลในรูปแบบนี้กัน
ข้อมูลจะมีอยู่ 2 แบบคือข้อความและภาพ แต่ทั้งสองก็มีโครงสร้างที่เหมือนกันคือมี
• avatar : URL ภาพโปรไฟล์ของเจ้าของข้อความนั้นๆ
• data : ข้อมูลที่เป็นข้อความหรือ URL ภาพ
• senderId : UID เจ้าของข้อความนั้นๆ (ได้จากตอน Log in ด้วย Firebase Authentication)
• type : ประเภทของข้อมูล ถ้าเป็นข้อความจะเป็น "text" แต่ถ้าเป็นภาพก็จะเป็น "image"
• username : ชื่อเจ้าของข้อความนั้นๆ
ดังนั้นเจ้าของบล็อกจึงสร้างคลาสที่ชื่อว่า Message ขึ้นมาทำหน้าที่เป็น POJO นั่นเอง
จะเห็นว่ามีการสร้าง String Constant สำหรับประเภทของข้อความด้วยโดยทำเป็น TYPE_IMAGE และ TYPE_TEXT เพื่อป้องกันการ Hard Code ด้วย String (ถ้ามีการเปลี่ยน Type ของข้อความในภายหลัง ก็จะได้มาแก้ไขที่นี่ที่เดียวเลย)
ซึ่งคลาส Message ก็จะถูกเรียกใช้ตอนที่รับ/ส่งข้อมูลระหว่าง Firebase Realtime Database และใช้ในการแสดงผลใน Adapter (ของ Recycler View)
ทีนี้ก็มาดูที่ ChatAdapter.java กันต่อ ซึ่งเป็น Adapter ที่ควบคุมการแสดง Item View ใน Recycler View นั่นเอง เนื่องจากรูปแบบของ Item View มีอยู่ 2 รูปแบบ (ข้อความของเรากับข้อความของคนอื่น) ดังนั้น ChatAdapter จึงต้องรองรับ View Type 2 แบบ ดังนี้
ตัวแปรสำคัญใน Adapter ก็จะอยู่ที่ uid และ messageList
ถ้า uid มีค่าตรงกับ senderId ของ Message ก็หมายความว่าข้อความนั้นเป็นแบบ TYPE_YOUR_MESSAGE แต่ถ้าค่าไม่ตรงกันก็จะกลายเป็น TYPE_OTHER_MESSAGE ทันที
ซึ่ง Type แต่ละแบบก็จะ Inflate Layout คนละตัวกัน (ใน onCreateViewHolder) แต่ไม่ว่าจะตัวไหนก็ตามก็ยังใช้ MessageViewHolder เหมือนกันอยู่นะ (ตามที่ตั้งใจไว้ในตอนแรกว่า Layout คนละแบบ แต่ View Holder จะต้องใช้ร่วมกันได้)
เพิ่มเติม - คลาส ChatAdapter สามารถสืบทอดจาก RecyclerView.Adapter โดยใช้เป็น MessageViewHolder โดยตรงเลยก็ได้เหมือนกันนะ
แต่การประกาศแบบนี้จะทำให้คลาส ChatAdapter ผูกขาดกับ MessageViewHolder จนเกินไป (แต่ก็ใช้ได้เช่นกัน ไม่ถือว่าผิด) ในอนาคตถ้ามี View Holder แบบอื่นเพิ่มเข้ามาก็จะใช้งานไม่ได้ ต้องกลับใช้ประกาศเป็น RecyclerView.ViewHolder อยู่ดี สรุปก็คือประกาศเผื่อไว้เฉยๆนั่นเอง
เนื่องจากข้อมูลจะมีแบบเป็น URL ของรูปด้วย นั่นหมายความว่าหน้า Chat ก็จะต้องส่งไฟล์รูปได้ด้วยเช่นกัน ซึ่งเจ้าของบล็อกก็หยิบ Library เข้ามาช่วยแทนการเขียนเอง โดยใช้ไลบรารีที่ชื่อว่า EZPhotoPicker
EZPhotoPicker รองรับการเลือกไฟล์ภาพจาก Gallery ในเครื่องและการถ่ายภาพจากกล้องโดยตรง ซึ่งปกติแล้วจะต้องเขียนอะไรในส่วนนี้เยอะมาก แต่เมื่อใช้ Library ตัวนี้ ก็จะเหลือโค้ดที่ต้องเขียนเพียงเท่านี้
Library ตัวนี้สามารถกำหนดได้ว่าจะเลือกแหล่งภาพจาก Gallery หรือ Camera รวมไปถึงรองรับการย่อขนาดไฟล์ภาพตามขนาดที่เราต้องการได้ด้วย (ในตัวอย่างกำหนดไว้ 900px) เมื่อไฟล์ภาพที่เลือกมีความกว้างหรือความสูงเกินที่กำหนดไว้ Library ตัวนี้ก็จะย่อให้เท่าที่กำหนดไว้ทันที (สามารถกำหนดค่าอย่างอื่นได้อีกหลายๆอย่างเลยล่ะ) ส่วนผลลัพธ์จากการเลือกไฟล์ภาพก็จะส่งมาที่ onActivityResult เหมือนเดิมนั่นแหละ ซึ่ง Library ตัวนี้ก็จะรวบคำสั่งตรงนี้ให้ง่ายขึ้นด้วยเช่นกัน (แค่เช็ค Request/Result Code แล้วดึงผลลัพธ์เป็นคลาส Bitmap จากคำสั่งที่ Library เตรียมไว้ให้แล้ว)
และในหน้า ChatActivity จะต้อง Log out ได้ เจ้าของบล็อกจึงเพิ่ม Menu option ไว้ด้วยเพื่อแสดงเป็นปุ่ม Log out
โดยปกติจะสามารถกำหนดได้ว่าจะให้ Menu Item ที่อยู่ใน Menu Option แสดงผลแบบไหนผ่าน Attribute ที่ชื่อว่า android:showAsAction แต่ถ้า Menu Option ถูกเรียกใช้งานใน Activity ที่สืบทอดจาก AppCompatActivity จะต้องกำหนดผ่าน app:showAsAction แทน (ถือว่าเป็นหนึ่งในปัญหายอดนิยมของ Menu Option ที่พบเจอได้บ่อยๆบน StackOverflow)
ข้อความที่ต้องใช้ในโปรเจคนี้ก็จะแยกเก็บไว้ใน strings.xml เพื่อให้สะดวกต่อการแก้ไขในทีหลัง
Dimension ที่กำหนดลงใน Layout XML ถ้าอันไหนต้องกำหนดเป็นค่า DP ก็จะให้เรียกจาก dimens.xml เพื่อให้สะดวกต่อการทำ Multiple Screen Supported ในวันหลัง
การใช้งาน Firebase หรือ Google Services บางตัวนั้นจะต้องใช้ไฟล์ google-services.json แต่ในโปรเจค Code Battle บน GitHub จะไม่ได้ใส่ไฟล์นี้ไว้ให้ เนื่องจากเป็นไฟล์ของใครของมัน
โปรเจค Code Battle ตัวนี้ยังไม่สามารถทำงานได้ทันทีนะ แต่ต้องตั้งค่านิดหน่อยทั้งในโปรเจคและบน Firebase Console ด้วย ซึ่งขอรวบไปเล่าให้ฟังกันต่อในบทความหน้าแทนนะ บทความนี้ก็เป็นการ Overview คร่าวๆไปก่อนว่ามีอะไรบ้างที่เจ้าของบล็อกหยิบมาใช้เพื่อให้โค้ดพร้อมสำหรับการ Code Battle ในงาน Firebase Dev Day
ดังนั้นใน LoginActivity และ ChatActivity ก็จะสืบทอดจาก BaseActivity อีกทีหนึ่ง เพื่อจะได้เรียกใช้คำสั่งสำหรับ Loading Dialog กับ Snackbar ง่ายๆ โดยการทำ Loading Dialog ก็จะเป็น Dialog แบบ Fragment Dialog ที่มีคลาสที่ชื่อว่า QuickDialog คอยควบคุมการทำงานอยู่เบื้องหลัง โดยใช้ Library ที่ชื่อว่า SmoothProgressBar เพื่อทำ Loading เป็นวงกลมหมุนติ๊วๆ
Dependency ของ Library ต่างๆที่ไม่เกี่ยวข้องกับ Firebaes โดยตรงก็จะมากองรวมกันอยู่ใน Library ตัวนี้ด้วยเช่นกัน
Library หลายๆตัวที่ใส่ไว้ในนี้จะถูกเรียกใช้ใน Module หลักด้วย ซึ่งเดี๋ยวเจ้าของบล็อกจะบอกให้อีกทีว่าตัวไหนใช้ตรงไหนของแอปฯ
Internet Permission ก็ถูกใส่ไว้ให้เรียบร้อยแล้วใน AndroidManifest.xml ของ Module ตัวนี้ จะได้ไม่ต้องไปนั่งใส่เองใน Module หลัก
ส่วนเรื่องสีที่ใช้ ปกติเจ้าของบล็อกจะมี Palette สีที่ใช้ทำงานส่วนตัวอยู่แล้ว ดังนั้นใน Library ตัวนี้ก็จะมี colors.xml ที่เต็มไปด้วยสีที่เจ้าของบล็อกมักจะใช้ แล้วเพิ่มสีที่เป็นธีมของงาน Code Battle เข้าไป อีกทีหนึ่ง
โดยสีเหล่านี้ก็จะถูกเรียกใช้จากใน Module หลักและ Library (จะไม่มีการใส่ค่าสีแบบดิบๆลงในโค้ดโดยตรง)
ขนาดหรือ Dimension พื้นฐานอย่าง Margin หรือ Font Size ก็จะเตรียมไว้ใน dimens.xml ด้วยเช่นกัน โดยจะมีหลายๆขนาดเพื่อให้ยืดหยุ่นกับการเรียกไปใช้งาน แบ่งเป็น Extra Extra Small, Extra Small, Small, Medium, Large, Extra Large, Extra Extra Large และ Extra Extra Extra Large
เพื่อไม่ให้โค้ดใน XML รก เจ้าของบล็อกก็จะมี Style ของ View พื้นฐานเตรียมไว้อยู่เช่นกัน โดยจะอยู่ใน styles.xml ของ Library นี่แหละ ก็จะมี Style ของ Text และ Button ในรูปแบบต่างๆเตรียมไว้ เพื่อให้เรียกใช้งานใน Layout XML ได้เลย จะได้ไม่ต้องมานั่งพิมพ์ Attribute ทีละตัวให้เปลืองบรรทัด
ในบางครั้ง Button ก็ไม่ต้องการพื้นหลัง แต่ก็ควรมี Selector ด้วย ดังนั้นก็จะมี Style ที่ชื่อว่า Borderless ของ Button ให้ด้วย ซึ่งสร้างจาก Shape XML นี่แหละ และปุ่มที่ใช้ใน Code Battle นี้ ก็ไม่ต้องการพื้นหลังด้วยเช่นกัน รวมไปถึง Shadow Gradient ที่ทำเป็น Shape XML ไว้ให้เรียกใช้งานได้เลย
App Module
ทีนี้มาดูกันที่ Module หลักบ้างว่าเจ้าของบล็อกเตรียมอะไรไว้แล้วมี Activity อยู่สองตัวคือ LoginActivity กับ ChatActivity โดยที่ ChatActivity นั้นจะมี Adapter และ View Holder ด้วย เพราะแสดงข้อความบน Recycler View รวมไปถึง Model ที่ชื่อว่า Message เอาไว้เก็บข้อมูลที่อยู่บน Firebase Realtime Database
ส่วน Dependency ที่เพิ่มเข้ามาก็จะเป็นของ Firebase และ Google Play Services เท่านั้น
เริ่มจากหน้า Log in ก่อนนะ
LoginActivity จะผูกไว้กับ Layout ที่ชื่อว่า activity_login.xml ที่ออกแบบไว้แบบนี้ในหน้า Log in ก็จะมีแค่ปุ่ม Log in เท่านั้นที่จะต้องมีการเขียนโค้ดเพิ่ม แต่ปุ่มที่ว่านี้ก็ไม่ได้สร้างขึ้นมาเองนะ ใช้ SignInButton ซึ่งเป็น View สำเร็จรูปจาก Google Play Services Auth
ทีนี้ข้ามมาดูที่ไฟล์ LoginActivity.java กันต่อเลย
เนื่องจากเจ้าของบล็อกไม่ชอบยัดโค้ดต่างๆลงใน onCreate ตูมเดียวเลย เดี๋ยวมันรก ดังนั้นก็จะแยกเป็น Method ยิบย่อยตามหน้าที่แบบนี้
เมื่อดูเฉพาะโค้ดของปุ่ม Log in จะมีแค่นี้
public class LoginActivity extends BaseActivity {
private SignInButton signInButton;
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
bindView();
setupView();
...
}
...
private void bindView() {
signInButton = (SignInButton) findViewById(R.id.btn_sign_in);
}
private void setupView() {
signInButton.setSize(SignInButton.SIZE_WIDE);
signInButton.setOnClickListener(onSignInClick());
}
private View.OnClickListener onSignInClick() {
return new View.OnClickListener() {
@Override
public void onClick(View v) {
signIn();
}
};
}
...
}
ผู้ที่หลงเข้ามาอ่านอาจจะรู้สึกแปลกๆตรง onSignInClick() ซึ่งเจ้าของบล็อกเขียนไว้แบบนี้
private View.OnClickListener onSignInClick() {
return new View.OnClickListener() {
@Override
public void onClick(View v) {
signIn();
}
};
}
ที่เขียนแบบนี้ก็เพราะว่าถ้าใช้ Lambda Expression หรือ Retrolambda แล้วโค้ดมันจะกระชับมากๆ
private View.OnClickListener onSignInClick() {
return v -> signIn();
}
แต่ในโปรเจคก็ไม่ได้ใส่ Retrolambda ไว้ให้ เพราะเผื่อว่าผู้ที่หลงเข้ามาอ่านบางคนจะไม่เข้าใจโค้ดในส่วนนี้ จึงใส่เป็นโค้ดแบบปกติไว้ให้แทนครับ
ต่อกันด้วยหน้า Chat
ChatActivity จะผูกไว้กับ Layout ที่ชื่อว่า activity_chat.xml ที่ออกแบบไว้แบบนี้ซึ่งจะประกอบไปด้วย Edit Text, Button และ Recycler View ซึ่งตรงนี้ไม่น่าจะต้องอธิบายอะไรมากนัก เพราะเป็น View พื้นฐานที่นักพัฒนาแอนดรอยด์แทบทุกคนได้ใช้งานกันอยู่แล้ว
สำหรับการแสดง Chat ด้วย Recycler View นั้นจะต้องสร้าง Item View สองแบบด้วยกันเพื่อแสดงข้อความคนละฝั่ง ซึ่งใน Code Battle ตกลงกันว่าจะให้ข้อความคนอื่นๆอยู่ฝั่งซ้าย และข้อความของตัวเองอยู่ฝั่งขวา
ดังนั้นเจ้าของบล็อกจึงสร้าง Layout สำหรับ Item View ทั้งสองแบบเป็นไฟล์ที่ชื่อว่า view_other_message_item.xml กับ view_your_message_item.xml
Layout ทั้งสองนั้นแทบจะเหมือนกันทั้งหมด ต่างกันแค่เรื่องชิดซ้าย/ขวาเท่านั้นเอง ดังนั้นเจ้าของบล็อกจึงกำหนด ID ของ View ให้เหมือนกันด้วยเลย
พอลองดูภาพตัวอย่างของ Code Battle ก็จะเห็นว่าตรงรูปโปรไฟล์นั้นเป็นวงกลม ส่วนข้อความก็จะมีพื้นหลังเป็นกล่องคำพูด
ทำยังไงดีล่ะ?
คำตอบก็คือใช้ Library นั่นแหละฮะ จะได้ไม่ต้องเสียเวลา
สำหรับรูปโปรไฟล์วงกลมจะใช้ CircleImageView จะได้ไม่ต้องมานั่งทำเอง (ถึงแม้ว่า Glide จะทำได้ก็ตาม) ส่วนกล่องข้อความก็ใช้ ChatMessageView ที่สามารถปรับตำแหน่งของลูกศรคำพูดได้ตามใจชอบ (ยืดหยุ่นต่อการนำไปใช้งานดี)
และเนื่องจาก Item View ทั้งสองแบบนั้นใช้ชื่อ ID เหมือนๆกัน และทำหน้าที่เหมือนๆกัน จึงสามารถสร้าง View Holder แค่ตัวเดียวเพื่อใช้กับ Item View ทั้งคู่ได้เลย เย้เย
ก่อนจะไปดูคลาส Adapter ขอพากลับมาดูที่คลาส Model ที่ชื่อว่า Message สำหรับรับข้อมูลจาก Firebase Realtime Database ก่อน ซึ่งเจ้าของบล็อกและคนอื่นๆได้ตกลงกันว่าจะเก็บข้อมูลในรูปแบบนี้กัน
ข้อมูลจะมีอยู่ 2 แบบคือข้อความและภาพ แต่ทั้งสองก็มีโครงสร้างที่เหมือนกันคือมี
• avatar : URL ภาพโปรไฟล์ของเจ้าของข้อความนั้นๆ
• data : ข้อมูลที่เป็นข้อความหรือ URL ภาพ
• senderId : UID เจ้าของข้อความนั้นๆ (ได้จากตอน Log in ด้วย Firebase Authentication)
• type : ประเภทของข้อมูล ถ้าเป็นข้อความจะเป็น "text" แต่ถ้าเป็นภาพก็จะเป็น "image"
• username : ชื่อเจ้าของข้อความนั้นๆ
ดังนั้นเจ้าของบล็อกจึงสร้างคลาสที่ชื่อว่า Message ขึ้นมาทำหน้าที่เป็น POJO นั่นเอง
public class Message {
public static final String TYPE_IMAGE = "image";
public static final String TYPE_TEXT = "text";
private String avatar;
private String data;
private String type;
private String username;
private String senderId;
...
}
จะเห็นว่ามีการสร้าง String Constant สำหรับประเภทของข้อความด้วยโดยทำเป็น TYPE_IMAGE และ TYPE_TEXT เพื่อป้องกันการ Hard Code ด้วย String (ถ้ามีการเปลี่ยน Type ของข้อความในภายหลัง ก็จะได้มาแก้ไขที่นี่ที่เดียวเลย)
ซึ่งคลาส Message ก็จะถูกเรียกใช้ตอนที่รับ/ส่งข้อมูลระหว่าง Firebase Realtime Database และใช้ในการแสดงผลใน Adapter (ของ Recycler View)
ทีนี้ก็มาดูที่ ChatAdapter.java กันต่อ ซึ่งเป็น Adapter ที่ควบคุมการแสดง Item View ใน Recycler View นั่นเอง เนื่องจากรูปแบบของ Item View มีอยู่ 2 รูปแบบ (ข้อความของเรากับข้อความของคนอื่น) ดังนั้น ChatAdapter จึงต้องรองรับ View Type 2 แบบ ดังนี้
public class ChatAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final int TYPE_YOUR_MESSAGE = 0;
private static final int TYPE_OTHER_MESSAGE = 1;
private List<Message> messageList;
private String uid;
...
public ChatAdapter(String uid) {
this.uid = uid;
}
public void setMessageList(List<Message> messageList) {
this.messageList = messageList;
}
...
@Override
public int getItemViewType(int position) {
Message message = messageList.get(position);
if (message.getSenderId() != null && message.getSenderId().equals(uid)) {
return TYPE_YOUR_MESSAGE;
} else {
return TYPE_OTHER_MESSAGE;
}
}
...
}
ตัวแปรสำคัญใน Adapter ก็จะอยู่ที่ uid และ messageList
ถ้า uid มีค่าตรงกับ senderId ของ Message ก็หมายความว่าข้อความนั้นเป็นแบบ TYPE_YOUR_MESSAGE แต่ถ้าค่าไม่ตรงกันก็จะกลายเป็น TYPE_OTHER_MESSAGE ทันที
ซึ่ง Type แต่ละแบบก็จะ Inflate Layout คนละตัวกัน (ใน onCreateViewHolder) แต่ไม่ว่าจะตัวไหนก็ตามก็ยังใช้ MessageViewHolder เหมือนกันอยู่นะ (ตามที่ตั้งใจไว้ในตอนแรกว่า Layout คนละแบบ แต่ View Holder จะต้องใช้ร่วมกันได้)
public class ChatAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
...
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == TYPE_YOUR_MESSAGE) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.view_your_message_item, parent, false);
return new MessageViewHolder(view);
} else {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.view_other_message_item, parent, false);
return new MessageViewHolder(view);
}
}
...
}
เพิ่มเติม - คลาส ChatAdapter สามารถสืบทอดจาก RecyclerView.Adapter โดยใช้เป็น MessageViewHolder โดยตรงเลยก็ได้เหมือนกันนะ
public class ChatAdapter extends RecyclerView.Adapter<MessageViewHolder> {
...
}
แต่การประกาศแบบนี้จะทำให้คลาส ChatAdapter ผูกขาดกับ MessageViewHolder จนเกินไป (แต่ก็ใช้ได้เช่นกัน ไม่ถือว่าผิด) ในอนาคตถ้ามี View Holder แบบอื่นเพิ่มเข้ามาก็จะใช้งานไม่ได้ ต้องกลับใช้ประกาศเป็น RecyclerView.ViewHolder อยู่ดี สรุปก็คือประกาศเผื่อไว้เฉยๆนั่นเอง
เนื่องจากข้อมูลจะมีแบบเป็น URL ของรูปด้วย นั่นหมายความว่าหน้า Chat ก็จะต้องส่งไฟล์รูปได้ด้วยเช่นกัน ซึ่งเจ้าของบล็อกก็หยิบ Library เข้ามาช่วยแทนการเขียนเอง โดยใช้ไลบรารีที่ชื่อว่า EZPhotoPicker
EZPhotoPicker รองรับการเลือกไฟล์ภาพจาก Gallery ในเครื่องและการถ่ายภาพจากกล้องโดยตรง ซึ่งปกติแล้วจะต้องเขียนอะไรในส่วนนี้เยอะมาก แต่เมื่อใช้ Library ตัวนี้ ก็จะเหลือโค้ดที่ต้องเขียนเพียงเท่านี้
public class ChatActivity extends BaseActivity {
...
private void chooseImage() {
EZPhotoPickConfig config = new EZPhotoPickConfig();
config.photoSource = PhotoSource.GALERY;
config.exportingSize = 900;
EZPhotoPick.startPhotoPickActivity(this, config);
}
...
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == EZPhotoPick.PHOTO_PICK_REQUEST_CODE &&
resultCode == RESULT_OK) {
try {
Bitmap pickedPhoto = new EZPhotoPickStorage(this).loadLatestStoredPhotoBitmap();
sendImage(pickedPhoto);
} catch (IOException e) {
e.printStackTrace();
onUploadPhotoFailure();
}
}
}
...
}
Library ตัวนี้สามารถกำหนดได้ว่าจะเลือกแหล่งภาพจาก Gallery หรือ Camera รวมไปถึงรองรับการย่อขนาดไฟล์ภาพตามขนาดที่เราต้องการได้ด้วย (ในตัวอย่างกำหนดไว้ 900px) เมื่อไฟล์ภาพที่เลือกมีความกว้างหรือความสูงเกินที่กำหนดไว้ Library ตัวนี้ก็จะย่อให้เท่าที่กำหนดไว้ทันที (สามารถกำหนดค่าอย่างอื่นได้อีกหลายๆอย่างเลยล่ะ) ส่วนผลลัพธ์จากการเลือกไฟล์ภาพก็จะส่งมาที่ onActivityResult เหมือนเดิมนั่นแหละ ซึ่ง Library ตัวนี้ก็จะรวบคำสั่งตรงนี้ให้ง่ายขึ้นด้วยเช่นกัน (แค่เช็ค Request/Result Code แล้วดึงผลลัพธ์เป็นคลาส Bitmap จากคำสั่งที่ Library เตรียมไว้ให้แล้ว)
และในหน้า ChatActivity จะต้อง Log out ได้ เจ้าของบล็อกจึงเพิ่ม Menu option ไว้ด้วยเพื่อแสดงเป็นปุ่ม Log out
โดยปกติจะสามารถกำหนดได้ว่าจะให้ Menu Item ที่อยู่ใน Menu Option แสดงผลแบบไหนผ่าน Attribute ที่ชื่อว่า android:showAsAction แต่ถ้า Menu Option ถูกเรียกใช้งานใน Activity ที่สืบทอดจาก AppCompatActivity จะต้องกำหนดผ่าน app:showAsAction แทน (ถือว่าเป็นหนึ่งในปัญหายอดนิยมของ Menu Option ที่พบเจอได้บ่อยๆบน StackOverflow)
ส่วนอื่นๆใน Module หลัก
เจ้าของบล็อกได้แยก Style สำหรับ LoginActivity และ ChatActivity ออกจากกัน เนื่องจากแสดงผลแตกต่างกัน แล้วไปกำหนดใน AndroidManifest.xml อีกทีข้อความที่ต้องใช้ในโปรเจคนี้ก็จะแยกเก็บไว้ใน strings.xml เพื่อให้สะดวกต่อการแก้ไขในทีหลัง
Dimension ที่กำหนดลงใน Layout XML ถ้าอันไหนต้องกำหนดเป็นค่า DP ก็จะให้เรียกจาก dimens.xml เพื่อให้สะดวกต่อการทำ Multiple Screen Supported ในวันหลัง
การใช้งาน Firebase หรือ Google Services บางตัวนั้นจะต้องใช้ไฟล์ google-services.json แต่ในโปรเจค Code Battle บน GitHub จะไม่ได้ใส่ไฟล์นี้ไว้ให้ เนื่องจากเป็นไฟล์ของใครของมัน
โปรเจค Code Battle ตัวนี้ยังไม่สามารถทำงานได้ทันทีนะ แต่ต้องตั้งค่านิดหน่อยทั้งในโปรเจคและบน Firebase Console ด้วย ซึ่งขอรวบไปเล่าให้ฟังกันต่อในบทความหน้าแทนนะ บทความนี้ก็เป็นการ Overview คร่าวๆไปก่อนว่ามีอะไรบ้างที่เจ้าของบล็อกหยิบมาใช้เพื่อให้โค้ดพร้อมสำหรับการ Code Battle ในงาน Firebase Dev Day