เนื้อหาต่อเนื่องจากตอนที่แล้ว ที่เราได้พูดถึงเกี่ยวกับ
การใช้งาน Sliver Widget ไปแล้ว เนื้อหานี้เราจะมาดูใน
ส่วนที่สอง เกี่ยวกับการใช้งาน Multi-child Layout ซึ่งประกอบ
ไปด้วย widget ต่างๆ ต่อไปนี้
เนื้อหาตอนที่แล้ว http://niik.in/1102
เนื้อหานี้ใช้โค้ดตัวอย่างเริ่มต้น จากบทความ ตามลิ้งค์นี้ http://niik.in/961
โดยใช้ โค้ดตัวอย่างจากส่วน เพิ่มเติมเนื้อหา ครั้งที่ 2
หน้าที่ของ Widget และ delegate ใน Multi-child Layout Widget
1. Column
เป็น widget ที่จัดเรียง widget ลูกในแนวตั้ง (column) จากบนลงล่าง โดยสามารถควบคุม
alignment และ spacing ระหว่าง widget ได้
มักใช้ในกรณีที่ต้องการจัดเรียง widget หลาย ๆ ตัวตามแนวตั้ง
ตัวอย่าง
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: GradientColumnExample(), ); } } class GradientColumnExample extends StatelessWidget { @override Widget build(BuildContext context) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, // จัดวาง container ให้ห่างกันเท่ากัน children: [ _buildGradientContainer(Colors.red, Colors.orange), _buildGradientContainer(Colors.blue, Colors.purple), _buildGradientContainer(Colors.green, Colors.yellow), ], ), ); } Widget _buildGradientContainer(Color startColor, Color endColor) { return Container( width: 200, // กำหนดความกว้างของ container height: 100, // กำหนดความสูงของ container decoration: BoxDecoration( gradient: LinearGradient( colors: [startColor, endColor], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(12.0), // เพิ่มมุมมนให้ container ), ); } }
ผลลัพธ์
2. CustomMultiChildLayout
เป็น widget ที่ช่วยในการกำหนดตำแหน่งและขนาดของ widget ลูกหลาย ๆ ตัวแบบกำหนดเอง
ตามที่ต้องการ
เหมาะสำหรับการออกแบบ layout ที่ซับซ้อนซึ่งต้องการควบคุมตำแหน่งของ widget ลูกแต่ละตัว
ด้วยตนเอง
ตัวอย่าง
class CustomMultiChildLayoutExample extends StatelessWidget { @override Widget build(BuildContext context) { return CustomMultiChildLayout( delegate: MyLayoutDelegate(), children: [ LayoutId( id: 'header', child: Container( color: Colors.blue, child: Text( 'Header', style: TextStyle(color: Colors.white, fontSize: 20), ), padding: EdgeInsets.all(16), ), ), LayoutId( id: 'body', child: Container( color: Colors.green, child: Text( 'Body', style: TextStyle(color: Colors.white, fontSize: 20), ), padding: EdgeInsets.all(16), ), ), LayoutId( id: 'footer', child: Container( color: Colors.red, child: Text( 'Footer', style: TextStyle(color: Colors.white, fontSize: 20), ), padding: EdgeInsets.all(16), ), ), ], ); } } class MyLayoutDelegate extends MultiChildLayoutDelegate { @override void performLayout(Size size) { // วางตำแหน่งและกำหนดขนาดของ header if (hasChild('header')) { final headerSize = layoutChild('header', BoxConstraints.loose(size)); positionChild('header', Offset(0, 0)); } // วางตำแหน่งและกำหนดขนาดของ body if (hasChild('body')) { final bodySize = layoutChild('body', BoxConstraints.loose(size)); positionChild('body', Offset(0, 100)); } // วางตำแหน่งและกำหนดขนาดของ footer if (hasChild('footer')) { final footerSize = layoutChild('footer', BoxConstraints.loose(size)); positionChild('footer', Offset(0, size.height - footerSize.height)); } } @override bool shouldRelayout(MyLayoutDelegate oldDelegate) => false; }
ผลลัพธ์
3. CarouselView
เป็น widget ที่ใช้ในการแสดงเนื้อหาหลาย ๆ ชิ้นในรูปแบบของ carousel โดยสามารถเลื่อนเพื่อดู
เนื้อหาอื่น ๆ ที่เรียงต่อกันในแนว horizontal หรือ vertical
มีฟังก์ชันการใช้งานที่สะดวก เช่น การตั้งค่าการเลื่อนอัตโนมัติ (auto-scrolling), การตั้งค่าความเร็ว
ในการเลื่อน, การแสดง indicator สำหรับหน้าที่กำลังแสดงอยู่ เป็นต้น เหมาะสำหรับใช้ในการแสดง
ภาพชุด, ข้อมูลโปรโมชั่น, หรือข้อมูลที่ต้องการนำเสนอเป็นชุด ๆ แบบ carousel
ตัวอย่าง
class CarouselExample extends StatefulWidget { const CarouselExample({super.key}); @override State<CarouselExample> createState() => _CarouselExampleState(); } class _CarouselExampleState extends State<CarouselExample> { final CarouselController controller = CarouselController(initialItem: 1); @override void dispose() { controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final double height = MediaQuery.sizeOf(context).height; return ListView( children: <Widget>[ ConstrainedBox( constraints: BoxConstraints(maxHeight: height / 2), child: CarouselView.weighted( controller: controller, itemSnapping: true, flexWeights: const <int>[1, 7, 1], children: ImageInfo.values.map((ImageInfo image) { return HeroLayoutCard(imageInfo: image); }).toList(), ), ), const SizedBox(height: 20), const Padding( padding: EdgeInsetsDirectional.only(top: 8.0, start: 8.0), child: Text('Multi-browse layout'), ), ConstrainedBox( constraints: const BoxConstraints(maxHeight: 50), child: CarouselView.weighted( flexWeights: const <int>[1, 2, 3, 2, 1], consumeMaxWeight: false, children: List<Widget>.generate(20, (int index) { return ColoredBox( color: Colors.primaries[index % Colors.primaries.length] .withOpacity(0.8), child: const SizedBox.expand(), ); }), ), ), const SizedBox(height: 20), ConstrainedBox( constraints: const BoxConstraints(maxHeight: 200), child: CarouselView.weighted( flexWeights: const <int>[3, 3, 3, 2, 1], consumeMaxWeight: false, children: CardInfo.values.map((CardInfo info) { return ColoredBox( color: info.backgroundColor, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Icon(info.icon, color: info.color, size: 32.0), Text(info.label, style: const TextStyle(fontWeight: FontWeight.bold), overflow: TextOverflow.clip, softWrap: false), ], ), ), ); }).toList()), ), const SizedBox(height: 20), const Padding( padding: EdgeInsetsDirectional.only(top: 8.0, start: 8.0), child: Text('Uncontained layout'), ), ConstrainedBox( constraints: const BoxConstraints(maxHeight: 200), child: CarouselView( itemExtent: 330, shrinkExtent: 200, children: List<Widget>.generate(20, (int index) { return UncontainedLayoutCard(index: index, label: 'Show $index'); }), ), ) ], ); } } class HeroLayoutCard extends StatelessWidget { const HeroLayoutCard({ super.key, required this.imageInfo, }); final ImageInfo imageInfo; @override Widget build(BuildContext context) { final double width = MediaQuery.sizeOf(context).width; return Stack( alignment: AlignmentDirectional.bottomStart, children: <Widget>[ ClipRect( child: OverflowBox( maxWidth: width * 7 / 8, minWidth: width * 7 / 8, child: Image( fit: BoxFit.cover, image: NetworkImage( 'https://flutter.github.io/assets-for-api-docs/assets/material/${imageInfo.url}'), ), ), ), Padding( padding: const EdgeInsets.all(18.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: <Widget>[ Text( imageInfo.title, overflow: TextOverflow.clip, softWrap: false, style: Theme.of(context) .textTheme .headlineLarge ?.copyWith(color: Colors.white), ), const SizedBox(height: 10), Text( imageInfo.subtitle, overflow: TextOverflow.clip, softWrap: false, style: Theme.of(context) .textTheme .bodyMedium ?.copyWith(color: Colors.white), ) ], ), ), ]); } } class UncontainedLayoutCard extends StatelessWidget { const UncontainedLayoutCard({ super.key, required this.index, required this.label, }); final int index; final String label; @override Widget build(BuildContext context) { return ColoredBox( color: Colors.primaries[index % Colors.primaries.length].withOpacity(0.5), child: Center( child: Text( label, style: const TextStyle(color: Colors.white, fontSize: 20), overflow: TextOverflow.clip, softWrap: false, ), ), ); } } enum CardInfo { camera('Cameras', Icons.video_call, Color(0xff2354C7), Color(0xffECEFFD)), lighting('Lighting', Icons.lightbulb, Color(0xff806C2A), Color(0xffFAEEDF)), climate('Climate', Icons.thermostat, Color(0xffA44D2A), Color(0xffFAEDE7)), wifi('Wifi', Icons.wifi, Color(0xff417345), Color(0xffE5F4E0)), media('Media', Icons.library_music, Color(0xff2556C8), Color(0xffECEFFD)), security( 'Security', Icons.crisis_alert, Color(0xff794C01), Color(0xffFAEEDF)), safety( 'Safety', Icons.medical_services, Color(0xff2251C5), Color(0xffECEFFD)), more('', Icons.add, Color(0xff201D1C), Color(0xffE3DFD8)); const CardInfo(this.label, this.icon, this.color, this.backgroundColor); final String label; final IconData icon; final Color color; final Color backgroundColor; } enum ImageInfo { image0('The Flow', 'Sponsored | Season 1 Now Streaming', 'content_based_color_scheme_1.png'), image1('Through the Pane', 'Sponsored | Season 1 Now Streaming', 'content_based_color_scheme_2.png'), image2('Iridescence', 'Sponsored | Season 1 Now Streaming', 'content_based_color_scheme_3.png'), image3('Sea Change', 'Sponsored | Season 1 Now Streaming', 'content_based_color_scheme_4.png'), image4('Blue Symphony', 'Sponsored | Season 1 Now Streaming', 'content_based_color_scheme_5.png'), image5('When It Rains', 'Sponsored | Season 1 Now Streaming', 'content_based_color_scheme_6.png'); const ImageInfo(this.title, this.subtitle, this.url); final String title; final String subtitle; final String url; }
ผลลัพธ์
4. Flow
เป็น widget ที่จัดเรียง widget ลูกในทิศทางที่กำหนด (เช่น ซ้ายไปขวา บนลงล่าง) โดยใช้การจัดการ
layout แบบกำหนดเอง
เหมาะสำหรับการสร้าง layout ที่ซับซ้อนหรือการจัดเรียง widget แบบ dynamic ที่ไม่สามารถ
จัดการได้ด้วย Row, Column หรือ Wrap
ตัวอย่าง
class CustomFlowExample extends StatelessWidget { @override Widget build(BuildContext context) { return Center( child: Flow( delegate: CustomFlowDelegate(), children: List.generate(10, (index) { return Container( width: 80, height: 80, color: Colors.primaries[index % Colors.primaries.length], child: Center( child: Text( 'Item $index', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), ), ), ); }), ), ); } } class CustomFlowDelegate extends FlowDelegate { @override void paintChildren(FlowPaintingContext context) { double x = 0.0; double y = 0.0; for (int i = 0; i < context.childCount; i++) { final childSize = context.getChildSize(i)!; if (x + childSize.width > context.size.width) { // เริ่มบรรทัดใหม่เมื่อความกว้างเกินขอบเขต x = 0; y += childSize.height; } context.paintChild(i, transform: Matrix4.translationValues(x, y, 0)); x += childSize.width; } } @override bool shouldRepaint(covariant FlowDelegate oldDelegate) { return false; } }
ผลลัพธ์
5. GridView
เป็น widget ที่ใช้ในการแสดง widget ลูกในรูปแบบตาราง (grid) โดยมีการแบ่งแถวและคอลัมน์
อัตโนมัติตามขนาดของหน้าจอ
มีให้เลือกใช้หลายรูปแบบ เช่น GridView.count, GridView.builder, GridView.extent
ตัวอย่าง
class GradientGridView extends StatelessWidget { @override Widget build(BuildContext context) { return GridView.builder( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 4, // จำนวนคอลัมน์ใน GridView crossAxisSpacing: 4.0, mainAxisSpacing: 4.0, childAspectRatio: 1.0, ), itemCount: 100, // จำนวนรายการใน GridView itemBuilder: (context, index) { // คำนวณเฉดสีที่แตกต่างกันไปในแต่ละรายการ final startColor = Color.lerp(Colors.blue, Colors.red, index / 100)!; final endColor = Color.lerp(Colors.green, Colors.yellow, index / 100)!; return Container( decoration: BoxDecoration( gradient: LinearGradient( colors: [startColor, endColor], begin: Alignment.topLeft, end: Alignment.bottomRight, ), ), child: Center( child: Text( 'Item ${index + 1}', style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, ), ), ), ); }, ); } }
ผลลัพธ์
6. IndexedStack
เป็น widget ที่วาง widget ลูกหลาย ๆ ตัวซ้อนกันในตำแหน่งเดียวกัน โดยสามารถเลือกแสดงได้
เพียงตัวเดียวในแต่ละครั้ง
มักใช้ในกรณีที่ต้องการแสดง widget ที่แตกต่างกันตามเงื่อนไข หรือในกรณีที่ต้องการสลับหน้าแสดงผล
ตัวอย่าง
class IndexedStackExample extends StatefulWidget { @override _IndexedStackExampleState createState() => _IndexedStackExampleState(); } class _IndexedStackExampleState extends State<IndexedStackExample> { int _selectedIndex = 0; @override Widget build(BuildContext context) { return Column( children: [ // การแสดงผล widget ที่ใช้ IndexedStack Expanded( child: IndexedStack( index: _selectedIndex, children: <Widget>[ _buildPage('Home Page', Colors.blue), _buildPage('Settings Page', Colors.green), _buildPage('Profile Page', Colors.orange), ], ), ), // ปุ่มสำหรับสลับหน้าจอ BottomNavigationBar( currentIndex: _selectedIndex, onTap: (index) { setState(() { _selectedIndex = index; }); }, items: [ BottomNavigationBarItem( icon: Icon(Icons.home), label: 'Home', ), BottomNavigationBarItem( icon: Icon(Icons.settings), label: 'Settings', ), BottomNavigationBarItem( icon: Icon(Icons.person), label: 'Profile', ), ], ), ], ); } Widget _buildPage(String title, Color color) { return Container( color: color, child: Center( child: Text( title, style: TextStyle(fontSize: 24, color: Colors.white), ), ), ); } }
ผลลัพธ์
7. LayoutBuilder
เป็น widget ที่ใช้ในการสร้าง layout แบบ dynamic โดยให้ผู้พัฒนากำหนด layout ตามขนาดของ
พื้นที่ที่มีอยู่
มีประโยชน์เมื่อ layout ของ widget ต้องการปรับเปลี่ยนตามขนาดของหน้าจอหรือพื้นที่ที่มี
ตัวอย่าง
class DynamicLayoutExample extends StatelessWidget { @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { // ขนาดของพื้นที่ที่มีอยู่ double width = constraints.maxWidth; double height = constraints.maxHeight; return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: width * 0.8, // กำหนดความกว้างเป็น 80% ของพื้นที่ที่มีอยู่ height: height * 0.4, // กำหนดความสูงเป็น 40% ของพื้นที่ที่มีอยู่ color: Colors.blue, child: Center( child: Text( '80% Widthn40% Height', style: TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold, ), textAlign: TextAlign.center, ), ), ), SizedBox(height: 20), // ระยะห่างระหว่าง Container Container( width: width * 0.5, // กำหนดความกว้างเป็น 50% ของพื้นที่ที่มีอยู่ height: height * 0.2, // กำหนดความสูงเป็น 20% ของพื้นที่ที่มีอยู่ color: Colors.red, child: Center( child: Text( '50% Widthn20% Height', style: TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold, ), textAlign: TextAlign.center, ), ), ), ], ), ); }, ); } }
ผลลัพธ์
8. ListBody
เป็น widget ที่จัดเรียง widget ลูกในแนวตั้งหรือแนวนอนตามลำดับ โดยไม่มีการเลื่อน (scrolling)
มักใช้ในกรณีที่ต้องการจัดกลุ่มของ widget หลายตัวในทิศทางเดียวกัน
* ไม่นิยมใช้ ควรใช้เป็น ListView แทน ซึ่งสามารถทำได้หลายอย่างมากกว่า
9. ListView
เป็น widget ที่ใช้ในการแสดงรายการของ widget หลาย ๆ ตัวในแนวตั้งหรือแนวนอน โดยสามารถ
เลื่อน (scroll) ได้
มีหลายรูปแบบ เช่น ListView.builder สำหรับการสร้างรายการแบบ dynamic
ตัวอย่าง
class GradientListView extends StatelessWidget { @override Widget build(BuildContext context) { return ListView.builder( itemCount: 100, // จำนวนรายการใน ListView itemBuilder: (context, index) { // คำนวณเฉดสีที่แตกต่างกันไปในแต่ละรายการ final startColor = Color.lerp(Colors.blue, Colors.red, index / 100)!; final endColor = Color.lerp(Colors.green, Colors.yellow, index / 100)!; return Container( height: 50, // กำหนดความสูงของแต่ละรายการ decoration: BoxDecoration( gradient: LinearGradient( colors: [startColor, endColor], begin: Alignment.topLeft, end: Alignment.bottomRight, ), ), child: Center( child: Text( 'Item ${index + 1}', style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, ), ), ), ); }, ); } }
ผลลัพธ์
10. Row
เป็น widget ที่จัดเรียง widget ลูกในแนวนอน (row) จากซ้ายไปขวา โดยสามารถควบคุม alignment
และ spacing ระหว่าง widget ได้
มักใช้ในกรณีที่ต้องการจัดเรียง widget หลาย ๆ ตัวตามแนวนอน
ตัวอย่าง
class GradientRowExample extends StatelessWidget { @override Widget build(BuildContext context) { return Center( child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, // จัดเรียง container ให้ห่างกันเท่ากัน children: [ _buildGradientContainer(Colors.red, Colors.orange), _buildGradientContainer(Colors.blue, Colors.purple), _buildGradientContainer(Colors.green, Colors.yellow), ], ), ); } Widget _buildGradientContainer(Color startColor, Color endColor) { return Container( width: 100, height: 100, decoration: BoxDecoration( gradient: LinearGradient( colors: [startColor, endColor], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(12.0), // เพิ่มมุมมน ), ); } }
ผลลัพธ์
11. Stack
เป็น widget ที่วาง widget ลูกหลาย ๆ ตัวซ้อนกันในตำแหน่งเดียวกัน โดยสามารถกำหนดตำแหน่งของ
widget แต่ละตัวได้
มักใช้ในกรณีที่ต้องการแสดง widget หลาย ๆ ตัวซ้อนทับกัน เช่น การวางรูปภาพกับข้อความบนรูปภาพ
ตัวอย่าง
class ExclusiveNews extends StatelessWidget { @override Widget build(BuildContext context) { return Center( child: Stack( alignment: Alignment.center, children: [ // รูปภาพด้านล่าง Image.network( 'https://www.peanutsquare.com/wp-content/uploads/2023/06/Flutter-3.0-Whats-New-and-How-to-Get-Started-jpg.webp', width: 300, height: 200, fit: BoxFit.cover, ), // ข้อความด้านบน Container( width: 300, height: 200, alignment: Alignment.center, decoration: BoxDecoration( color: Colors.black.withOpacity(0.6), // พื้นหลังโปร่งใส ), child: Text( 'Exclusive News: Flutter 3.0 Released!', style: TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold, ), textAlign: TextAlign.center, ), ), ], ), ); } }
ผลลัพธ์
12. Table
เป็น widget ที่ใช้ในการสร้างตาราง โดยสามารถกำหนดจำนวนแถวและคอลัมน์ได้ตามต้องการ
มักใช้ในกรณีที่ต้องการแสดงข้อมูลในรูปแบบของตารางที่มีโครงสร้างชัดเจน
ตัวอย่าง
class PremierLeagueTable extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(16.0), child: Table( border: TableBorder.all(), columnWidths: { 0: FixedColumnWidth(50.0), 1: FlexColumnWidth(), 2: FixedColumnWidth(50.0), 3: FixedColumnWidth(50.0), 4: FixedColumnWidth(50.0), }, children: [ TableRow( decoration: BoxDecoration(color: Colors.grey[300]), children: [ _buildTableHeader('Pos'), _buildTableHeader('Team'), _buildTableHeader('P'), _buildTableHeader('W'), _buildTableHeader('Pts'), ], ), _buildTableRow(1, 'Manchester City', 38, 29, 90), _buildTableRow(2, 'Liverpool', 38, 28, 89), _buildTableRow(3, 'Chelsea', 38, 21, 74), _buildTableRow(4, 'Tottenham', 38, 22, 71), // เพิ่มทีมอื่น ๆ ได้ตามต้องการ ], ), ); } TableRow _buildTableRow(int position, String team, int played, int wins, int points) { return TableRow( children: [ _buildTableCell(position.toString()), _buildTableCell(team), _buildTableCell(played.toString()), _buildTableCell(wins.toString()), _buildTableCell(points.toString()), ], ); } Widget _buildTableHeader(String text) { return Padding( padding: const EdgeInsets.all(8.0), child: Text( text, style: TextStyle(fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), ); } Widget _buildTableCell(String text) { return Padding( padding: const EdgeInsets.all(8.0), child: Text( text, textAlign: TextAlign.center, ), ); } }
ผลลัพธ์
13. Wrap
เป็น widget ที่จัดเรียง widget ลูกในทิศทางที่กำหนด (เช่น ซ้ายไปขวา บนลงล่าง) และสามารถ
wrap ไปยังแถวถัดไปได้หากพื้นที่ไม่พอ
เหมาะสำหรับกรณีที่ต้องการแสดงรายการ widget ที่มีขนาดไม่คงที่ และไม่ต้องการให้ overflow
ตัวอย่าง
class WrapExample extends StatelessWidget { @override Widget build(BuildContext context) { return SingleChildScrollView( child: Padding( padding: EdgeInsets.all(16.0), child: Wrap( spacing: 8.0, // ระยะห่างระหว่าง widget ลูกในแนวนอน runSpacing: 8.0, // ระยะห่างระหว่างแถวในแนวตั้ง alignment: WrapAlignment.center, // การจัดแนว widget ลูกในแนวตั้ง runAlignment: WrapAlignment.center, // การจัดแนวแถวในแนวตั้ง children: List.generate(20, (index) { return Container( width: 100 + (index % 5) * 20, // ขนาดของ widget ลูกที่เปลี่ยนแปลง height: 100, // ความสูงของ widget ลูก color: Colors.primaries[index % Colors.primaries.length], // สีของ widget ลูก child: Center( child: Text( 'Item ${index + 1}', style: TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold, ), ), ), ); }), ), ), ); } }
ผลลัพธ์
ตัวอย่างโค้ดข้างต้นเป็นแนวทางให้เราสามารถนำไปประยุกต์ใช้งานเพิ่มเติมต่อได้