ต้องมาเริ่มทำแอปสำหรับ Tablet และ Foldable Phone กันแล้ว แค่แก้นิดเดียว (?!) ด้วย Flutter
สำหรับมือถือแบบจอพับได้นั้น ในวงการยังถือว่าเป็นตลาดที่ค่อนข้างเล็กและใหม่อยู่ แต่ก็มีแนวโน้มเป็นที่สนใจกันมากขึ้น แม้กระทั่ง Apple ยังมีข่าวหลุดมาเรื่อยๆ ว่ากำลังซุ่มพัฒนาอยู่เช่นกัน ในฐานะผู้พัฒนามือถือสาย Flutter ที่มีสโลแกนว่า “เขียนครั้งเดียว ลงได้หลายที่” นั้น เราก็ทำให้มันซัพพอร์ตจอพับด้วยเลยแล้วกัน ยังไงก็เป็น Android อยู่แล้ว
แต่ก่อนจะทำนั้น อยากจะบอกว่าพอเราทำงานประเภทนี้นานๆ เข้า นอกจาก Programming แล้ว เราต้องนึกถึงแรงที่ทุ่มลงไปด้วย ไม่ใช่ทุ่มไปเยอะ แต่คนได้ใช้นิดเดียว มันก็ไม่คุ้ม ซึ่งจอพับได้จะเข้าข่ายกรณีแบบนี้เช่นกัน แต่ถ้าผมจะบอกว่าสิ่งที่แก้มีแค่นิดเดียวล่ะ? น่าลองไหม
นิดเดียว?!?! จริงเหรอ อย่ามาโม้ เดี๋ยวในบทความนี้จะมาแสดงให้เห็นว่า ผมไม่ได้โม้!!! แค่แก้นิดเดียว ก็ทำให้จอพับหน้าตาสวยขึ้นเยอะ ยิ่งไปกว่านั้นการที่เราซัพพอร์ตจอพับได้ Google Play เค้าก็จะช่วยทำให้แอปเราหาง่ายขึ้นใน Store ด้วยนะ ซึ่งเราก็ได้อ้างบทความนี้กับ Product Manager เพื่อจะได้ทำ 😘
จากบทความด้านบน สรุปสั้นๆ ให้ว่า Google Play จะช่วยโปรโมทแอปที่ทำมารองรับจอใหญ่ ไม่ว่าจะเป็น Tablet, Foldable หรือ Desktop ก็ตาม ซึ่งทั้งหมดนั้นคือ Android นั่นเอง
ทั้งนี้ผมจะขอเล่าเน้นๆ เจาะไปที่ Tablet กับ Foldable นะครับ เอา Desktop ไปวางไว้ไกลๆ ก่อน เพราะการจะทำสนับสนุน Desktop ได้นั้น ต้องมีเรื่อง Mouse/Keyboard เพิ่มขึ้นมา ทั้งการคลิกขวา และไฮไลท์ เช่น Mouse Hover ฟังแล้วไม่นิดเดียวละ ฉะนั้นข้ามมม
จริงๆ แล้วเวลาพูดถึง Tablet ก็รวมถึง Android Tablet และ iPad ด้วยนะครับ ส่วนจอพับ ถ้ากางออกมาเต็มที่ มันก็คือ Tablet แหละ ฉะนั้นในเคสนี้ เราแค่พูดอ้อมๆ ว่ามาทำแอปให้สนับสนุน Tablet กัน เท่านี้จอพับก็ได้ประโยชน์ไปด้วย
ถัดไปเราจะว่าด้วยประเด็นหลักๆ ที่ต้องพิจารณาในการพัฒนาแอปให้ดูดีบนจอใหญ่
Continuity — ความต่อเนื่อง
ตัวแอปควรจะต้องเก็บ State ไว้ว่า User อยู่ที่ตรงไหน ทำอะไรไว้อยู่ และยังต้องทำให้ State ของเราเก็บไว้ ไม่ว่า User จะพับจอหรือกางจอก็ตาม อีกทั้งเราควรจะต้องซัพพอร์ตการหมุนจออีกด้วย
แต่โดยปกติสำหรับจอมือถือนั้น ผมจะล็อคหน้าจอไว้ไม่ให้หมุน เพราะในกรณีที่คนจะใช้แอปเราแบบแนวนอนนั้นน้อยมากๆ ถ้าไม่ใช่แอปพิเศษ เช่น พวกแอปดูหนัง หรือเล่นเกม ด้วยเหตุนี้เอง ผมแนะนำว่าล็อคจอมือถือไว้แนวตั้งก็พอ หลายแอปใหญ่ก็ทำแบบนี้กัน เช่น Facebook เป็นต้น (แถมไม่ต้องมานั่งเทส + นั่ง Dev เพิ่ม)
อย่างไรก็ตามสำหรับ iPad ผมแนะนำให้ Tick ให้หมดเลยครับ เพราะ iPadOS มีความสามารถพิเศษคือการทำ Split Screen ได้ ซึ่งหากเราไม่ Tick ส่วนนี้จะทำงานไม่ได้ ประกอบกับจอ Tablet ที่ค่อนข้างใหญ่ ทำให้คนส่วนใหญ่เล่นทั้งแนวตั้งและแนวนอน แล้วแต่ชอบเลย
แอป Flutter ที่ขึ้นใหม่โดยทั่วไปจะมีการซัพพอร์ต Continuity อยู่แล้ว จะมีแต่พวกโปรเจคเก่าๆ ส่วนใหญ่ที่ใช้ไม่ได้ทันที เท่าที่เจอมาคือจะต้องไปแก้บางส่วนหรืออัพเดต Lib บางตัว อย่างของผมมีการไป Set ค่าที่ AndroidManifest.xml เพื่อให้ State ไม่เริ่มใหม่เวลาที่มีการพับหรือกางจอ (ตอนแรก State ในโปรเจค MAKE by KBank ที่ทำ ก็ Clear State เหมือนกัน) นำโค้ดตามนี้ไปใส่ได้เลยครับ
<activity
android:name=".MainActivity"
android:resizeableActivity="true"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout">
</activity>
แค่เพิ่มเข้าไปในไฟล์ AndroidManifest.xml เวลาเราพับจอหรือกางจอ แอปก็จะไม่ Restart ตัวเองแล้ว
คงจุดที่ Scroll ไป
จะมี PageStorageKey API ที่ให้ใช้คู่กับ CustomScrollView หรือ ListView ต่างๆ ใส่เข้าไปตรงที่ Key นะครับ Flutter จะจำ State ให้เอง ว่าก่อนพับจอหรือหมุนจอนั้น User Scroll ไปที่ตำแหน่งไหน พอกางจอ ก็จะกลับมาที่เดิมให้อัตโนมัติ
Responsive & Adaptive Layout
ในการทำให้ User Experience ลื่นปรื้ด สดชื่น แบบฉ่ำๆ สำหรับคนจอใหญ่นั้น เราควรใช้พื้นที่จอให้คุ้มค่าที่สุด แทนที่จะมองว่า “เจอจอใหญ่เหรอ ก็ขยายรูปหรือขนาดให้เต็มจอเหมือนกับมือถือสิ” แม้ดูเป็นทางออกง่ายๆ แต่เราขอไม่ใช้วิธีนั้น เราจะมาเน้นใส่เนื้อหาเพิ่มเข้าไปแทน โดยคงขนาดของรูปหรือ Font เท่าเดิม ตามหลักการต่อไปนี้
1. อย่าไปขยายทุกอย่าง บางอันอาจจะ Make Sense แต่บางอันไม่เหมาะเลย ยกตัวอย่างที่เคยเจอคือ Scale Font กับรูปขึ้นมาตามขนาดของหน้าจอ เช่น มือถือ Font Size 14 แต่พอไปจอใหญ่ เช่น iPad ถ้าเราคำนวณตามขนาดจอ แล้วขยายขึ้นไป บางที Font Size อาจจะขึ้นไป 20–22 จนทำให้ดูก็ไม่สวย แถมพื้นที่ที่ใช้ก็สิ้นเปลือง ให้ Font Size เท่าเดิมไปแหละครับ ไม่ต้องขยาย เราแค่ใช้พื้นที่ที่เหลือให้มีประโยชน์ ด้วยการเพิ่ม Widget อื่นๆ ที่จำเป็นเข้ามาแทน
2. ใช้วิธีตรวจสอบว่าจอเราเป็นประเภทอะไร เป็นขนาดมือถือ, Tablet/Fold หรือขนาดจอ Desktop ด้วยวิธีนี้
enum ScreenSize { small, normal, large, extraLarge }
ScreenSize getSize(BuildContext context) {
final deviceWidth = MediaQuery.sizeOf(context).shortestSide;
if (deviceWidth > 900) return ScreenSize.extraLarge;
if (deviceWidth > 600) return ScreenSize.large;
if (deviceWidth > 300) return ScreenSize.normal;
return ScreenSize.small;
}
(เนื่องจากตอนนี้เราสนใจจะทำแค่มือถือกับ Tablet ผมแนะนำให้ Handle Case แค่ Normal กับ Large ก็พอครับ)
3. ใช้ Lib มาช่วย ในการตรวจสอบว่าเครื่องเป็น Foldable Phone หรือ Tablet ซึ่ง Lib ตัวนี้จะ Detect และมี Callback สำหรับ hingeAngleEvents เพื่อตรวจสอบว่า Device เครื่องนี้มี Sensor ฝาพับหรือไม่ หรือถ้าอยากล้ำไปกว่านี้ ก็มี hasHingeAngleSensor ไว้สำหรับตรวจสอบว่าตอนนี้ฝาพับเปิดหรือปิดที่องศาเท่าไหร่ แต่ผมขอข้ามอันนี้ 😆 เพราะอย่างที่บอกว่าอยากลงแรงน้อยๆ และมองว่าไม่ได้เป็นสิ่งจำเป็นนัก ด้วยตัวผมยังนึกความสำคัญไม่ออกว่าเราจะเอาค่าเหล่านี้ไปทำอะไรให้เกิดประโยชน์ดีนะ
4. ใช้คอนเซ็ปต์ในการดีไซน์จอใหญ่ ด้วยการวาง ListView คู่กับ DetailsView ยกตัวอย่างแบบแอป iPad ด้านล่าง จะเห็นว่าในจอใหญ่เราสามารถนำหน้า A ที่เป็น ListView กับหน้า B ที่เป็น DetailsView มาวางไว้ในหน้าเดียวกัน เมื่อกดหน้า A ทางซ้ายแล้ว เราก็ให้ไปอัพเดตหน้า B ทางขวา แทนที่จะขึ้นหน้าใหม่ ซึ่งเราสามารถทำแบบเดียวกันได้กับแอป Flutter เรา
โดยปกติ ListView จะกินพื้นที่ประมาณ 1 ใน 3 ของจอทั้งหมด ส่วน 2 ใน 3 ที่เหลือจะเป็น DetailsView แต่วิธีนี้จะไม่ค่อยเหมาะกับจอพับเท่าไร เพราะเวลากางออก จอจะใหญ่เป็น 2 เท่าเสมอ (ไม่งั้นพอพับแล้วจะเหลื่อมๆ กัน) ทำให้เวลาที่ทำจอ Ratio 1/3 : 2/3 นั้นดูแปลกๆ ในกรณีนี้ผมจึงเลือกทำขนาดจอครึ่งๆ เผื่อจอพับไปด้วยเลย แต่ถ้าใครอยากทำแยกไปอีก เป็น Tablet Layout นึง จอพับอีก Layout นึงก็ได้ ส่วนผมขอจบแค่ครึ่งๆ พอ 😆
5. บางหน้าที่ไม่สามารถทำแบบ ListView/DetailsView ได้ ให้ปรับ Layout ให้เหมาะสมกับขนาดใหญ่ ด้วยการทำ Content ให้มากขึ้นแทน ตรงนี้จะไม่มีสูตรตายตัว ใช้ Sense เอาได้เลย ยกตัวอย่าง MAKE by KBank แทนที่เราจะขยาย GridView ให้คงที่ที่ 2 Items/Row เสมอนั้น ผมแก้ให้ทำเป็น 4 Items/Row แทนเมื่อเป็นจอใหญ่
Old Design vs New Design: Unfold Mode
6. Popup นั้นไม่จำเป็นต้องทำเต็มจอเหมือนกับมือถือนะครับ เพราะมันจะกินพื้นที่และออกมาดูแปลกๆ เดิมเราอาจจะทำตามดีไซน์ว่าให้ทำ Padding จากซ้ายขวา 16 Pixel ซึ่งพอไปขึ้นจอใหญ่ไม่เวิร์คแน่นอน ดังนั้นผมมักจะกำหนด Popup ไม่ให้ยาวเกิน 480 Pixel เนื่องจากมือถือส่วนใหญ่จะอยู่แถวๆ 300–375 Pixel จะมีแค่เคส Samsung S24 Ultra ที่ผมเจอจอกว้างประมาณ 440 Pixel ในขณะที่บน Tablet ตัว Popup จะอยู่กลางๆ จอแบบรูปตัวอย่าง ซึ่งผมใช้ ConstrainedBox Widget ในการกำหนดขนาดเอา
Phone vs Tablet Mode: BottomSheet Popup
Phone vs Tablet: Dialog Old Design
แต่ถ้าเรากำหนด Padding ไว้ที่ 16 Pixel จะเห็นว่าหน้าตาใน Tablet จะดูใหญ่เทอะทะ พื้นที่โล่งแปลกๆ ทันที
Phone vs Tablet: Dialog New Design
หลังจากที่กำหนดไม่ให้ขนาดเกิน 480 Pixel แล้ว ตัว Popup ดูกระชับขึ้นมาทันตา ถามว่า 480 เลขนี้มาจากไหน ไม่มีที่ไปที่มาครับ แต่ที่แน่ๆ Google บอกไว้ว่าอย่าให้เกิน 560 Pixel ซึ่งจากที่ผมลองไปลองมา 480 กำลังงาม ได้ออกมาตามรูปด้านล่างแทนที่จะบวมๆ แบบด้านบน แค่เอา ConstrainedBox ไปคลุมเองก็จบ
ทั้งหมดนี้เป็นส่วนที่ผมมองว่าแก้นิดเดียว เพราะที่ผมทำคือลดการ Scale ที่ไม่จำเป็น, เพิ่ม ConstrainedBox ไม่ให้เกิน 480px สำหรับ Dialog ทุกประเภท, แก้ค่าต่างๆ สำหรับ Tablet Mode และปรับการเคลียร์ State เวลาพับหรือกางหน้าจอ รวมแล้วผมใช้เวลาแค่ประมาณ 3–4 ชั่วโมงเท่านั้น จากโปรเจคส่วนตัวที่ไม่ได้ซัพพอร์ต Tablet ใดๆ จนออกมาดูสวยงาม ทั้งนี้ถ้าคุณมองว่ามีเวลาเหลือแล้วอยากไปต่อ ผมคงแนะนำให้ทำ Adaptive Layout เพิ่มครับ เอา Widget A กับ B มาอยู่ในหน้าเดียวกัน หรือปรับ UI สำหรับ Tablet ให้เปลี่ยนไปเลย แต่อันนั้นจะใช้ Effort เยอะละ เพราะต้องขึ้น UI ใหม่
จริงๆ ตอนนี้ Android 14 ก็มี Jetpack ที่ไว้ซัพพอร์ตจอคู่แล้ว ซึ่งผมกำลังลองเล่นอยู่ คือเราเปิดจอด้านในของมือถือ และเปิดจอด้านนอกของมือถือไว้พร้อมๆ กัน
จากที่ลองหลายๆ แบบ ตอนนี้ยังนึก Use Case ดีๆ ไม่ออก แต่ที่อยากลองจะเป็นในส่วนของแอปการเงิน ประมาณว่าจอด้านในแสดงข้อมูลส่วนตัวของเรา ในขณะที่จอด้านนอกแสดงข้อมูลที่จำเป็นให้กับฝั่งตรงข้าม เช่น โอนเงินแล้ว สลิปขึ้นอีกฝั่ง ทั้งนี้มาคิดๆ ดูแล้ว ผมต้องโอนเงินแล้วทำมือตั้งฉากกับพื้น เพื่อที่ฝั่งตรงข้ามดูสลิปเลยเหรอ ฟังแล้วลำบากจัง ต้องมา Explore กันต่อไปครับ
สำหรับใครที่ชื่นชอบบทความนี้ อย่าลืมกดติดตาม Medium: KBTG Life เรามีสาระความรู้และเรื่องราวดีๆ จากชาว KBTG พร้อมเสิร์ฟให้ที่นี่ที่แรก