ในการพัฒนาแอปด้วย flutter สิ่งที่พบและมีความสำคัญก็คือ
การจัดการเกี่ยวกับเลเอาท์ของแอป กรณีที่มีความซับซ้อนหรือ
มีการใช้งาน widget ต่างๆ เพิ่มมากขึ้นเรื่อยๆ ปัญหาเกี่ยวกับพื้นที่
หรือส่วนแสดงข้อมูลมักจะเกิดขึ้น เช่น เนื้อหาหรือข้อมูล เกินขอบเขต
ของ widget หรือใช้ตัว widget ที่ไม่รองรับการขยายขนาดอัตโนมัติ
ดังนั้น เนื่อหานี้ เราจะกลับมาทบทวนส่วนต่างๆ นี้ และจะเป็นการบันทึกไว้
เป็นแนวทาง กลับมาใช้งานหรือมาเป็นตัวช่วยในการแก้ป้ญหา ที่อาจจะ
เกิดขึ้นในอนาคตได้
เนื้อหานี้ใช้โค้ดตัวอย่างเริ่มต้น จากบทความ ตามลิ้งค์นี้ http://niik.in/961
โดยใช้ โค้ดตัวอย่างจากส่วน เพิ่มเติมเนื้อหา ครั้งที่ 2
เกี่ยวกับ Layout Widget ใน Flutter
ใน Flutter, Layout widgets แบ่งออกเป็น 3 ประเภทหลัก ได้แก่ Single-child layout widgets,
Multi-child layout widgets และ Sliver widgets แต่ละประเภทมีหน้าที่และความหมายต่างกัน ดังนี้:
1. Single-child Layout Widgets
เป็น widgets ที่สามารถมีลูกได้เพียงตัวเดียว ซึ่งหมายความว่า widgets เหล่านี้จะวาง layout หรือ
จัดเรียงเฉพาะ widget ลูกเพียงตัวเดียวเท่านั้น
ใช้ในการจัดการ layout ของ widget ลูกตัวเดียว เช่น การกำหนดขนาด ตำแหน่ง หรือขอบเขตของ
widget ลูก
ตัวอย่าง:
Container: ใช้ในการปรับขนาด สี พื้นหลัง ขอบเขต ฯลฯ ของ widget ลูก
Center: จัดวาง widget ลูกไว้ที่กลางหน้าจอ
Padding: เพิ่มพื้นที่ว่างรอบๆ widget ลูก
2. Multi-child Layout Widgets
เป็น widgets ที่สามารถมีลูกได้หลายตัว ซึ่งหมายความว่า widgets เหล่านี้สามารถจัดเรียง widget
ลูกหลายๆ ตัวพร้อมกันได้
ใช้ในการจัดการ layout ของ widget หลายตัว เช่น การจัดเรียงในแนวนอน แนวตั้ง การซ้อนทับกัน
หรือการกระจายพื้นที่ระหว่าง widget ลูก
ตัวอย่าง:
Row: จัดเรียง widget ลูกในแนวนอน
Column: จัดเรียง widget ลูกในแนวตั้ง
Stack: ซ้อน widget ลูกหลายตัวทับกัน
3. Sliver Widgets
เป็น widgets ที่ใช้สำหรับการจัดการ scrollable areas โดยเฉพาะ เช่น ListView หรือ GridView
แต่มีความยืดหยุ่นมากขึ้นและสามารถจัดการการ scroll ที่ซับซ้อนได้มากขึ้น
ใช้ในการสร้างส่วนต่างๆ ที่สามารถ scroll ได้ เช่น การสร้าง list ที่เลื่อนได้อย่างมีประสิทธิภาพ หรือการ
จัดการพื้นที่ที่สามารถขยายหดได้ในขณะที่มีการ scroll
ตัวอย่าง:
SliverList: ใช้ในการสร้าง list ที่สามารถ scroll ได้
SliverGrid: ใช้ในการสร้าง grid ที่สามารถ scroll ได้
SliverAppBar: AppBar ที่สามารถย่อหรือขยายได้เมื่อมีการ scroll
ความแตกต่าง:
Single-child layout widgets เหมาะสำหรับการจัดการ layout ของ widget ลูกตัวเดียว
ในขณะที่ Multi-child layout widgets ใช้สำหรับจัดการ layout ของ widget หลายตัวพร้อมกัน
Sliver widgets ต่างจากสองประเภทแรกเพราะมันใช้สำหรับการจัดการส่วนที่สามารถ scroll ได้
ซึ่งเหมาะสำหรับการสร้าง UI ที่ซับซ้อนและมีประสิทธิภาพในด้านการเลื่อนหน้าจอ
*Layout widgets ทั้งสามประเภทใน Flutter (Single-child layout widgets, Multi-child
layout widgets, และ Sliver widgets) สามารถใช้ร่วมกันในหน้าเดียวได้อย่างอิสระ การใช้ widgets
เหล่านี้ร่วมกันในหน้าเดียวกันเป็นเรื่องปกติในการพัฒนาแอปพลิเคชันด้วย Flutter เพื่อสร้างโครงสร้างของ
UI ที่ซับซ้อนและหลากหลาย
เนื้อหานี้เรามาดูกันที่ตัวอย่างตัวสุดท้าย คือ Sliver Widget
หน้าที่ของ Widget และ delegate ใน Sliver Widget
จะแสดงรายละเอียดหน้าที่การทำงานของแต่ละตัวตามลำดับดังนี้
1. CustomScrollView
เป็น widget ที่ช่วยให้เราสร้างเลเอาท์ที่สามารถเลื่อน (scroll) ได้อย่างยืดหยุ่น โดยสามารถรวม
sliver ต่าง ๆ (เช่น รายการ, กริด, ส่วนหัวที่ติดตามการเลื่อน) ไว้ใน CustomScrollView เพื่อสร้าง
การเลื่อนที่ซับซ้อนมากขึ้น
2. SliverAppBar
เป็น widget ที่ใช้สร้างแถบแอปที่สามารถขยาย (expand) และย่อ (collapse) ได้ตามการเลื่อนของ
เนื้อหาใน CustomScrollView โดยทั่วไปจะใช้เพื่อแสดงแถบชื่อหรือแถบเครื่องมือ (toolbar) ที่มีฟังก์ชัน
การเลื่อนที่ติดตามกับ CustomScrollView
3. SliverFixedExtentList
เป็น widget ที่ใช้สร้างรายการ (list) ที่เลื่อนขึ้นลงได้ ซึ่งความสูงของแต่ละรายการถูกกำหนดไว้แน่นอน
(itemExtent) ซึ่งช่วยเพิ่มประสิทธิภาพในการเลื่อน โดยเฉพาะเมื่อมีรายการจำนวนมาก
4. SliverList
เป็น widget ที่ใช้สร้างรายการ (list) ที่เลื่อนขึ้นลงได้ โดยความสูงของแต่ละรายการสามารถแตกต่างกัน
ตามเนื้อหาได้ จึงมีความยืดหยุ่นมากกว่า SliverFixedExtentList แต่มีความซับซ้อนในการคำนวณ
เลเอาท์มากขึ้น
5. SliverChildBuilderDelegate
เป็น delegate ที่ใช้ใน sliver เช่น SliverList หรือ SliverGrid เพื่อสร้างรายการโดยการเรียก
callback (builder) สำหรับแต่ละรายการ ช่วยในการสร้างรายการแบบไดนามิก (เมื่อเลื่อนถึงจุดนั้นจริง ๆ)
โดยไม่ต้องสร้างทุกรายการล่วงหน้า
6. SliverChildListDelegate
เป็น delegate ที่ใช้สร้างรายการจาก list ที่กำหนดไว้ล่วงหน้า (fixed list) ซึ่งเหมาะกับกรณีที่มีจำนวน
รายการคงที่และไม่มาก
7. SliverPadding
เป็น widget ที่ใช้ในการเพิ่มพื้นที่ว่าง (padding) รอบ ๆ sliver ที่อยู่ภายใน CustomScrollView
โดยทำงานคล้ายกับ Padding widget แต่ใช้กับ sliver โดยเฉพาะ
8. SliverPersistentHeader
เป็น widget ที่ใช้สร้างส่วนหัวที่สามารถยึดติดอยู่ด้านบนของหน้าจอ (pinned) หรือสามารถขยาย-ย่อ
(expand-collapse) ได้ตามการเลื่อน โดยทั่วไปจะใช้แสดงข้อมูลสำคัญที่ต้องการให้แสดงตลอดเวลาหรือ
เปลี่ยนขนาดตามการเลื่อน
9. SliverToBoxAdapter
เป็น widget ที่ช่วยในการแปลง widget ปกติที่ไม่ใช่ sliver ให้สามารถวางอยู่ใน CustomScrollView
ได้ เช่น การใช้เพื่อแสดง widget ทั่วไป (เช่น Container หรือ Text) ภายใน CustomScrollView
10. SliverGrid
เป็น widget ที่ใช้ในการแสดงรายการข้อมูลในรูปแบบตาราง (grid) ภายใน CustomScrollView
ซึ่งสามารถปรับแต่งจำนวนคอลัมน์หรือขนาดของแต่ละไอเท็มใน grid ได้ SliverGrid เหมาะกับการ
แสดงข้อมูลที่ต้องการจัดในลักษณะเป็นแถวและคอลัมน์ ซึ่งสามารถรองรับข้อมูลจำนวนมากและเลื่อน
ขึ้นลงได้อย่างลื่นไหล
SliverGridDelegate: เป็น delegate ที่ใช้ในการจัดการการวางตำแหน่งของแต่ละไอเท็มใน
grid โดยจะต้องกำหนดว่าต้องการให้ grid แสดงผลในลักษณะใด เช่น ขนาดของไอเท็ม ความกว้าง
ของคอลัมน์ หรือระยะห่างระหว่างไอเท็ม
SliverGridDelegateWithFixedCrossAxisCount: เป็น delegate ที่กำหนดจำนวนคอลัมน์
ที่คงที่ (fixed number of columns) และขนาดของแต่ละไอเท็มจะถูกคำนวณอัตโนมัติตามพื้นที่ที่เหลือ
อยู่ของหน้าจอ
SliverGridDelegateWithMaxCrossAxisExtent: เป็น delegate ที่กำหนดขนาดสูงสุดของ
ไอเท็มในแต่ละคอลัมน์ โดยจำนวนคอลัมน์จะถูกปรับอัตโนมัติตามความกว้างของหน้าจอและขนาดของไอเท็ม
แต่ละ widget เหล่านี้มีหน้าที่เฉพาะใน Flutter ที่ช่วยในการสร้างเลเอาท์ที่สามารถเลื่อนได้อย่างยืดหยุ่น
โดย CustomScrollView ทำหน้าที่เป็นคอนเทนเนอร์สำหรับ sliver ต่าง ๆ ส่วน sliver แต่ละตัวก็มีหน้าที่
และคุณสมบัติที่เฉพาะเจาะจง เช่น การสร้างแถบแอปที่ขยายได้ (SliverAppBar), การสร้างรายการที่มีความ
สูงเท่ากัน (SliverFixedExtentList), หรือการเพิ่มพื้นที่ว่างรอบ ๆ sliver (SliverPadding)
ตัวอย่างโค้ดไฟล์ home.dart
import 'package:flutter/material.dart'; class Home extends StatefulWidget { static const routeName = '/home'; const Home({Key? key}) : super(key: key); @override State<StatefulWidget> createState() { return _HomeState(); } } class _HomeState extends State<Home> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Home'), leading: IconButton( icon: Icon(Icons.menu), onPressed: () { Scaffold.of(context).openDrawer(); }, ), ), body: CustomScrollView( slivers: <Widget>[ const SliverAppBar( pinned: true, expandedHeight: 250.0, flexibleSpace: FlexibleSpaceBar( title: Text('Demo'), ), ), SliverPersistentHeader( pinned: true, delegate: _SliverAppBarDelegate( minHeight: 60.0, maxHeight: 150.0, child: Container( color: Colors.pink, child: Center(child: Text('Sliver Persistent Header')), ), ), ), SliverPadding( padding: EdgeInsets.all(16.0), sliver: SliverGrid( gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 200.0, mainAxisSpacing: 10.0, crossAxisSpacing: 10.0, childAspectRatio: 5.0, ), delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return Container( alignment: Alignment.center, color: Colors.teal[100 * (index % 9)], child: Text('Grid Item $index'), ); }, childCount: 20, ), ), ), SliverToBoxAdapter( child: SizedBox( height: 100.0, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: 10, itemBuilder: (BuildContext context, int index) { return Container( width: 100.0, margin: EdgeInsets.all(8.0), color: Colors.orange[100 * (index % 9)], child: Center(child: Text('Horizontal $index')), ); }, ), ), ), SliverPadding( padding: EdgeInsets.symmetric(vertical: 16.0), sliver: SliverFixedExtentList( itemExtent: 50.0, delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return Container( alignment: Alignment.center, color: Colors.lightBlue[100 * (index % 9)], child: Text('Fixed List Item $index'), ); }, childCount: 10, ), ), ), SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return Container( alignment: Alignment.center, margin: EdgeInsets.symmetric(vertical: 5.0), height: 60.0, color: Colors.purple[100 * (index % 9)], child: Text('List Item $index'), ); }, childCount: 10, ), ), ], ), ); } } class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { _SliverAppBarDelegate({ required this.minHeight, required this.maxHeight, required this.child, }); final double minHeight; final double maxHeight; final Widget child; @override double get minExtent => minHeight; @override double get maxExtent => maxHeight; @override Widget build( BuildContext context, double shrinkOffset, bool overlapsContent) { return SizedBox.expand(child: child); } @override bool shouldRebuild(_SliverAppBarDelegate oldDelegate) { return maxHeight != oldDelegate.maxHeight || minHeight != oldDelegate.minHeight || child != oldDelegate.child; } }
โค้ดตัวอย่าง เราสามารถนำเอาไปเป็นแนวทางการปรับเลเอาท์หน้าแอปของเรา ให้สามารถแสดงออกมา
แบบได้หลากหลาย และยืดหยุ่น