ListView เป็นตัว layout ที่ช่วยเอา widgets มาเรียงในแนวตั้งหรือแนวนอนแบบแถวเดียว โดยหากตัว widgets ที่จะเอามาแสดงมีขนาดเกินกว่าพื้นที่ของ Parent มันจะเพิ่ม scroll bar ให้สามารถเลื่อนดูได้ ขนาดของ widgets ที่เอามาแสดงใน ListView ไม่จำเป็นต้องมีขนาดเท่ากันทั้งหมด จริง ๆ มันก็ไม่ต่างจากการใช้แทนการเลื่อนหน้าดูข้อมูลธรรมดา ๆ ที่สามารถใช้งานตัว SingleChildScrollView ที่มี Row หรือ Column เป็นตัวจัด layout ให้
constructor ที่ใช้สร้าง ListView มีดังนี้
itemBuilder
separatorBuilder
SliverChildBuilderDelegate
ในการสร้าง listListView สามารถแสดงได้ทั้งแนวตั้งและแนวนอน โดยค่าเริ่มต้นคือแนวตั้ง ต้องการให้แสดง widgets อะไรบ้างก็ใส่เข้าไปใน children
เหมาะกับการแสดง widget ที่มีจำนวนไม่มาก
ListView({
Key? key,
Axis scrollDirection = Axis.vertical,
List<Widget> children = const <Widget>[],
})
ตัวอย่างแบบง่าย ๆ สร้าง widget ที่มีความสูง 80 และ 100 สำหรับแสดงใน ListView
import 'package:flutter/material.dart';
void main() {
Widget xWidget(double x) => Container(
height: x,
decoration: BoxDecoration(
color: Colors.green, border: Border.all(color: Colors.yellow)),
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Text("This is xWidget $x"),
));
var listView = ListView(
children: [xWidget(80), xWidget(100), xWidget(100), xWidget(80)]);
runApp(MaterialApp(
home: Scaffold(
body: listView,
),
));
}
ตัวอย่างการสร้าง ListView แบบง่าย ๆ
อ่ะ ลองเปลี่ยนเป็นแนวตั้งดู เพิ่ม scrollDirection: Axis.horizontal เหมือนจะง่าย แต่กลายเป็นว่า scroll ไม่ได้ กลายเป็น Row ธรรมดา 😅
var listView = ListView(
scrollDirection: Axis.horizontal,
children: [xWidget(80), xWidget(100), xWidget(100), xWidget(80)]);
พอปรับเป็นแนวตั้งบน desktop เลื่อนไม่ได้ซะงั้น
วิธีแก้ไข หากแอปไม่ได้ทำงานบนมือถือพวก Android iOS หรืออุปกรณ์ที่รองรับการสัมผัสด้วยนิ้ว ต้องไปตั้งค่าเพิ่มเติมในส่วนของการใช้ scroll bar
PointerDeviceKind.mouse
จะสามารถทำให้คลิกค้างเพื่อเอาไว้เลื่อน ListView ได้import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
void main() {
Widget xWidget(double x) => Container(
height: x,
decoration: BoxDecoration(
color: Colors.green, border: Border.all(color: Colors.yellow)),
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Text("This is xWidget $x"),
));
var scrollController = ScrollController(); // for Scrollbar and ListView
var listView = Scrollbar(
controller: scrollController,
child: ListView(
controller: scrollController,
scrollDirection: Axis.horizontal,
children: [xWidget(80), xWidget(100), xWidget(100), xWidget(80)]),
);
runApp(MaterialApp(
scrollBehavior: MaterialScrollBehavior().copyWith(dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse // add to use drag mouse to scroll
}),
home: Scaffold(
body: listView,
),
));
}
ListView.builder()
สำหรับกำหนด callback เพื่อใช้สร้าง widget ที่จะใช้แสดงใน ListView ผ่าน itemBuilder ข้อดีของการสร้างรายการแบบ on demand คือ ประสิทธิภาพของแอปจะดีกว่าแบบที่สร้างทีเดียวแล้วส่งไปให้ ListView เลย ตัว framework จะสร้างแค่พอแสดงบนหน้าจอและเผื่อการเลื่อนหน้าจอขึ้นลงเท่านั้น และยังสามารถใช้งานการรายการที่ไม่ทราบจำนวนล่วงหน้า รวมถึงรายการแบบไม่รู้จบได้อีกด้วย
ตัวอย่างการสร้างรายการเป็นข้อความจำนวน 100 รายการ
import 'package:flutter/material.dart';
void main() {
var listView = ListView.builder(
itemCount: 100,
itemBuilder: (BuildContext context, int index) {
return Text('item $index');
},
);
runApp(MaterialApp(
home: Scaffold(
body: listView,
),
));
}
การใช้ ListView.builder()
ในการใช้งานทั่วไปสามารถใช้ ListTile เพื่อแสดงข้อความ สี icon และอื่น ๆ เพื่อความสวยงามและประแต่งใช้ในงานทั่ว ๆ ไปได้ทันที
var listView = ListView.builder(
itemCount: 100,
itemBuilder: (BuildContext context, int index) {
return ListTile(leading: Icon(Icons.info), title: Text("item $index"));
},
);
การใช้ ListTile
ListView.separated
การสร้าง widget ที่จะใช้เป็นตัวแบ่ง จะกำหนดใน separatorBuilder
ดังตัวอย่างต่อไปนี้จะใช้ Divider เพื่อเป็นเส้นแบ่งระหว่างรายการ โดยรายการที่แสดงต้องมีจำนวนที่แน่นอน นั้นคือต้องระบุ itemCount
ด้วยเสมอ
var listView = ListView.separated(
itemCount: 100,
itemBuilder: (BuildContext context, int index) {
return ListTile(leading: Icon(Icons.info), title: Text("item $index"));
},
separatorBuilder: (BuildContext context, int index) {
return const Divider();
},
);
การสร้างตัวแบ่งรายการด้วย Divider()
ในการสร้างรายการ widget ที่ใช้แสดงใน ListView แบบ lazily based เช่น การสร้างด้วย ListView.builder()
การคงสถานะ หรือ state ของ widget จะสามารถกำหนดได้จากคำสั่ง addAutomaticKeepAlives
โดยค่าปริยายจะเป็น true
นั้นคือ ทุก widget ที่สร้างขึ้น จะถูกใส่ไว้ใน AutomaticKeepAlive เพื่อแจ้ง framework ให้รู้ว่าให้เก็บค่า State ของ widget เมื่อถูกถอดออกจาก widget tree เมื่อมันไม่ได้แสดงบน ListView แล้ว
ในกรณีที่ตัว widget ที่แสดงใน ListView ไม่ได้มีความจำเป็นต้องเก็บ State แนะนำว่าให้กำหนดค่า addAutomaticKeepAlives: false
ตัวอย่าง จะเป็นการสร้าง StatefulWidget ชื่อ ListTileColor
ขึ้นมา โดยเมื่อคลิกหรือแตะมัน มันจะทำการสลับสีจาก ขาว แดง เขียว น้ำเงิน วนไปเรื่อย ๆ ค่าสีจะถูกเก็บไว้ใน enum TileColor
ที่สร้างขึ้น ตัว ListTileColor
จะมี override ตัว initState()
และ dispose()
เพื่อพิมพ์ข้อความ log เมื่อมีการสร้างและลบ State
import 'package:flutter/material.dart';
void main() {
var listView = ListView.separated(
addAutomaticKeepAlives: true,
itemCount: 100,
itemBuilder: (BuildContext context, int index) {
return ListTileColor(index: index); // ListTile for State test
},
separatorBuilder: (BuildContext context, int index) {
return const Divider();
},
);
runApp(MaterialApp(
home: Scaffold(
body: listView,
),
));
}
// color table for Tile ---------------------------------------------
enum TileColor {
white(Colors.white),
red(Colors.red),
green(Colors.green),
blue(Colors.blue);
final Color color;
const TileColor(this.color);
}
// ListTile for State test ------------------------------------------
class ListTileColor extends StatefulWidget {
final int index;
const ListTileColor({super.key, required this.index});
@override
State<ListTileColor> createState() => _ListTileColorState();
}
class _ListTileColorState extends State<ListTileColor> {
late int _tileColor; // index of enum TileColor
// callback for click or tap the ListTile to change color index
void _changeColor() => setState(() => _tileColor = (_tileColor + 1) % TileColor.values.length);
@override
void initState() {
super.initState();
_tileColor = TileColor.white.index;
log('init state ${widget.index}');
}
@override
void dispose() {
log('dispose state ${widget.index}');
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _changeColor,
child: ListTile(
leading: Icon(Icons.info_outline),
title: Text("Item index ${widget.index}"),
tileColor: TileColor.values[_tileColor].color,
),
);
}
}
ลองทดสอบคลิกเพื่อกำหนดสี แล้วเลื่อนรายการไปมา ผลคือ สีที่ทำไว้หายหมด
สามารถลองเล่นได้ที่ dartpad.dev
วิธีการให้ตัว ListView คง State เอาไว้แม้ว่าจะถูกเลื่อนหน้าจนหลุดช่วงที่จะใช้งานไปแล้วก็ตาม
addAutomaticKeepAlives: true
ซึ่งค่าปริยายเป็นค่า true
อยู่แล้ว ตัว framework จะทำการหุ้มตัว widget ด้วย AutomaticKeepAlive ให้อัตโนมัติbool get wantKeepAlive
ถ้าคืนค่าเป็น true
จะทำการเก็บค่า State เอาไว้ ถ้าเป็น false
จะทำการ dispose เมื่อตัว widget หลุดไปจากหน้าจอbuild()
ของ widget จำเป็นต้องเพิ่มคำสั่งเพื่อเรียก super.build()
ด้วย ถ้าไม่ใส่ตัว complier จะเตือนตัวอย่างส่วนที่แก้ไขเพื่อให้ตัว widget คง State เอาไว้
class _ListTileColorState extends State<ListTileColor> with AutomaticKeepAliveClientMixin {
// ....
Widget build(BuildContext context) {
super.build(context);
// ....
}
@override
bool get wantKeepAlive => true;
}
ในการเขียนแอปให้มีประสิทธิภาพ หาก widget ไม่จำเป็นต้องเก็บ State ในขณะใช้งาน ก็ให้คืนค่า wantKeepAlive => false
สลับไปมาตามความจำเป็น โดยเมื่อต้องการเปลี่ยนค่านี้ ให้เรียกคำสั่ง updateKeepAlive()
ด้วยเสมอ เพื่อให้ตัว framework ดึงค่าล่าสุดไปใช้งาน
ตัวอย่างโปรแกรมที่เพิ่มส่วนของ AutomaticKeepAlive เข้าไป และมีการเลือกที่จะเก็บ State เฉพาะ widget ที่ถูกคลิกเลือกสีอื่นที่ไม่ใช่สีขาวเท่านั้น
import 'dart:developer';
import 'package:flutter/material.dart';
void main() {
var listView = ListView.separated(
addAutomaticKeepAlives: true,
itemCount: 100,
itemBuilder: (BuildContext context, int index) {
return ListTileColor(index: index);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider();
},
);
runApp(MaterialApp(
home: Scaffold(
body: listView,
),
));
}
// color table for Tile ---------------------------------------------
enum TileColor {
white(Colors.white),
red(Colors.red),
green(Colors.green),
blue(Colors.blue);
final Color color;
const TileColor(this.color);
}
// ListTile for State test ------------------------------------------
class ListTileColor extends StatefulWidget {
final int index;
const ListTileColor({
super.key,
required this.index,
});
@override
State<ListTileColor> createState() => _ListTileColorState();
}
class _ListTileColorState extends State<ListTileColor>
with AutomaticKeepAliveClientMixin {
late int _tileColor;
bool _saveState = false;
@override
void initState() {
super.initState();
_tileColor = TileColor.white.index;
log('init state ${widget.index}');
}
@override
void dispose() {
log('dispose state ${widget.index}');
super.dispose();
}
// callback for click or tap the ListTile to change color index
void _changeColor() => setState(() {
_tileColor++;
if (_tileColor >= TileColor.values.length) {
//default color index then there is no need to store status values.
_tileColor = 0;
_saveState = false;
updateKeepAlive();
} else {
if (!_saveState) {
_saveState = true;
updateKeepAlive();
}
}
});
@override
Widget build(BuildContext context) {
super.build(context);
return GestureDetector(
onTap: _changeColor,
child: ListTile(
leading: Icon(Icons.info_outline),
title: Text("Item index ${widget.index}"),
tileColor: TileColor.values[_tileColor].color,
),
);
}
@override
bool get wantKeepAlive => _saveState;
}
ผลการทำงานของ KeepAlive
สามารถลองเล่นได้ที่ dartpad.dev