Nếu bạn đã từng xây dựng bất kì một ứng dụng mobile với Android và IOS nào trước đây. Bạn sẽ cảm thấy việc tạo ListView trong Flutter đơn giản và dễ dàng thế nào. Trong bài viết này, tôi sẽ làm các ví dụ đơn giản để các bạn có thể hiểu một số cách sử dụng phổ biến của ListView.
Trước tiên, chúng ta sẽ xem xét các loại ListViews được xây dựng sẵn. Sau đó sẽ cùng tìm hiểu cách style cho các Items. Cuối cùng là hướng dẫn các thao tác thêm, xoá các item trong list.
Mục lục
Setup
Start một new project Flutter. Đặt tên project là flutter_listview.
Mở file main.dart và thay bằng đoạn code sau:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'ListViews',
theme: ThemeData(
primarySwatch: Colors.teal,
),
home: Scaffold(
appBar: AppBar(title: Text('ListViews')),
body: BodyLayout(),
),
);
}
}
class BodyLayout extends StatelessWidget {
@override
Widget build(BuildContext context) {
return _myListView(context);
}
}
// replace this function with the code in the examples
Widget _myListView(BuildContext context) {
return ListView();
}

Note: _myListView()
ở dòng cuối cùng là nơi chúng ta sẽ thêm các ví dụ cho bài hôm nay. Chỉ cần thay đổi code bên trong thẻ ListView.
Các loại ListView cơ bản
Static ListView
Nếu bạn có một danh sách ngắn các items không thay đổi, bạn có thể sử dụng ListView constructor mặc định để tạo một ListView cơ bản. Điều này rất hữu ích khi chúng ta muốn tạo một settings menu page….
Thêm đoạn code sau vào trong _myListView():
Widget _myListView(BuildContext context) {
return ListView(
children: <Widget>[
ListTile(
title: Text('Dog'),
),
ListTile(
title: Text('Cat'),
),
ListTile(
title: Text('Rabbit'),
),
],
);
}
Sau khi save lại, chúng ta sẽ thấy hình ảnh sau:

Đây là một ListView cơ bản nhất. Children của ListView là các ListTiles. ListTile là một widget được build sẵn để thể hiện Một title trong ListView cơ bản. Tuy nhiên một ListTile cũng có thể bao gồm subtitles, icons và images.
Nếu bạn muốn có thêm một dòng phân cách giữa các items, bạn có thể dùng ListTile.divideTiles:
Widget _myListView(BuildContext context) {
return ListView(
children: ListTile.divideTiles(
context: context,
tiles: [
ListTile(
title: Text('Dog'),
),
ListTile(
title: Text('Cat'),
),
ListTile(
title: Text('Rabbit'),
),
],
).toList(),
);
}
Lúc này, ListView của chúng ta sẽ trông như thế này:

Dynamic ListView
Khi sử dụng Static ListView. Tất cả các elements sẽ được tạo cùng một lúc. Điều này tốt khi sử dụng với một short list nhưng khi ta có một long list thì điều đó lại không phù hợp. Khi đó chúng ta nên tạo một ListView động bằng cách sử dụng ListView.builder()
constructor. Khi đó ListView sẽ chỉ render các phần tử xuất hiện trên màn hình.
Thay đổi đoạn code trong _myListView():
Widget _myListView(BuildContext context) {
// backing data
final europeanCountries = ['Albania', 'Andorra', 'Armenia', 'Austria',
'Azerbaijan', 'Belarus', 'Belgium', 'Bosnia and Herzegovina', 'Bulgaria',
'Croatia', 'Cyprus', 'Czech Republic', 'Denmark', 'Estonia', 'Finland',
'France', 'Georgia', 'Germany', 'Greece', 'Hungary', 'Iceland', 'Ireland',
'Italy', 'Kazakhstan', 'Kosovo', 'Latvia', 'Liechtenstein', 'Lithuania',
'Luxembourg', 'Macedonia', 'Malta', 'Moldova', 'Monaco', 'Montenegro',
'Netherlands', 'Norway', 'Poland', 'Portugal', 'Romania', 'Russia',
'San Marino', 'Serbia', 'Slovakia', 'Slovenia', 'Spain', 'Sweden',
'Switzerland', 'Turkey', 'Ukraine', 'United Kingdom', 'Vatican City'];
return ListView.builder(
itemCount: europeanCountries.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(europeanCountries[index]),
);
},
);
}
Khi đó, trên màn hình mobile sẽ xuất hiện:

itemCount sẽ thông báo cho ListView số lượng phần tử trong list Items. itemBuilder
function là function tạo danh sách động các phần tử khi chúng xuất hiện trên màn hình. BuildContext cung cấp context
parameter và item position là index
parameter. Index giúp chúng ta thuận tiện trong việc truy cập các phần tử trong list.
Infinite list
Flutter làm cho việc tạp một danh sách vô hạn trở nên đơn giản. Chỉ cần xóa itemCount
parameter khỏi hàm ListView.builder
Constructor.
Widget _myListView(BuildContext context) {
return ListView.builder(
itemBuilder: (context, index) {
return ListTile(
title: Text('row $index'),
);
},
);
}

Bạn có thể cuộn cả ngày và bạn sẽ không bao giờ đi đến phần tử cuối. Đó là cách quá dễ dàng.
Nếu bạn muốn thêm dấu phân cách giữa các ô trong ListView động, bạn có thể sử dụng ListView.separated
.
Widget _myListView(BuildContext context) {
return ListView.separated(
itemCount: 1000,
itemBuilder: (context, index) {
return ListTile(
title: Text('row $index'),
);
},
separatorBuilder: (context, index) {
return Divider();
},
);
}

Horizontal ListView
Để tạo một ListView theo chiều ngang, tất cả những gì bạn cần làm là đặt thuộc tính scrollDirection
là horizontal:
Widget _myListView(BuildContext context) {
return ListView.builder(
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 1.0),
width: 50,
color: Colors.tealAccent,
child: Text('$index'),
);
},
);
}

ListTile customization
Phần lớn các ListView không chỉ hiển thị đơn giản các dòng Text mà còn kèm theo icons hay images. Để làm điều đó, chúng ta cùng xét một ví dụ đơn giản:
Widget _myListView(BuildContext context) {
return ListView(
children: <Widget>[
ListTile(
leading: Icon(Icons.wb_sunny),
title: Text('Sun'),
),
ListTile(
leading: Icon(Icons.brightness_3),
title: Text('Moon'),
),
ListTile(
leading: Icon(Icons.star),
title: Text('Star'),
),
],
);
}

Bạn cũng có thể add icon vào phần cuối của item nếu bạn sử dụng thuộc tính trailing.
ListTile(
leading: Icon(Icons.wb_sunny),
title: Text('Sun'),
trailing: Icon(Icons.keyboard_arrow_right),
),

Chúng ta cũng có thể sử dụng image thay thế cho icon với CircleAvatar widget:
Widget _myListView(BuildContext context) {
return ListView(
children: <Widget>[
ListTile(
leading: CircleAvatar(
backgroundImage: AssetImage('assets/sun.jpg'),
),
title: Text('Sun'),
),
ListTile(
leading: CircleAvatar(
backgroundImage: AssetImage('assets/moon.jpg'),
),
title: Text('Moon'),
),
ListTile(
leading: CircleAvatar(
backgroundImage: AssetImage('assets/stars.jpg'),
),
title: Text('Star'),
),
],
);
}
Để code có thể chạy, chúng ta cần thêm image vào project. Bạn có thể sử dụng NetworkImage(imageUrl)
thay vì AssetImage(path)
. Tạo một thư mục assets trong thư mục gốc của project. Sau đó thêm các ảnh sau:



Đăng kí thư mục assets trong file pubspec.yaml:
flutter:
assets:
- assets/
Run lại ứng dụng và chúng ta sẽ được:

Để thêm phụ đề cho thẻ ListTile chúng ta thêm subtitle argument:
ListTile(
leading: CircleAvatar(
backgroundImage: AssetImage('assets/sun.jpg'),
),
title: Text('Sun'),
subtitle: Text('93 million miles away'), // <-- subtitle
),

Cards
Cards là một cách tuyệt vời để làm cho ListView của bạn trông sang trọng. Tất cả những gì bạn phải làm là bọc ListTile của mình bằng một Cards widget.
Widget _myListView(BuildContext context) {
final titles = ['bike', 'boat', 'bus', 'car',
'railway', 'run', 'subway', 'transit', 'walk'];
final icons = [Icons.directions_bike, Icons.directions_boat,
Icons.directions_bus, Icons.directions_car, Icons.directions_railway,
Icons.directions_run, Icons.directions_subway, Icons.directions_transit,
Icons.directions_walk];
return ListView.builder(
itemCount: titles.length,
itemBuilder: (context, index) {
return Card( // <-- Card widget
child: ListTile(
leading: Icon(icons[index]),
title: Text(titles[index]),
),
);
},
);
}

Bạn có thể thay đổi shadow của Cards bằng thuộc tính elevation. Bạn cũng nên thử các thuộc tính shape
và margin
.
Custom list item
Nếu ListTile không đủ cho nhu cầu của bạn, bạn có thể tự design tile cho chính mình. Xem ví dụ sau:
Widget _myListView(BuildContext context) {
// the Expanded widget lets the columns share the space
Widget column = Expanded(
child: Column(
// align the text to the left instead of centered
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('Title', style: TextStyle(fontSize: 16),),
Text('subtitle'),
],
),
);
return ListView.builder(
itemBuilder: (context, index) {
return Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: <Widget>[
column,
column,
],
),
),
);
},
);
}

Touch detection
Nếu bạn đang sử dụng ListTile, tất cả những gì bạn phải làm để phản hồi lại thao tác chạm của người dùng là thêm một onTap
hoặc onLongTouch
callback.
Widget _myListView(BuildContext context) {
return ListView(
children: <Widget>[
ListTile(
title: Text('Sun'),
trailing: Icon(Icons.keyboard_arrow_right),
onTap: () {
print('Sun');
},
),
ListTile(
title: Text('Moon'),
trailing: Icon(Icons.keyboard_arrow_right),
onTap: () {
print('Moon');
},
),
ListTile(
title: Text('Star'),
trailing: Icon(Icons.keyboard_arrow_right),
onTap: () {
print('Star');
},
),
],
);
}

Nếu bạn đang làm việc với custom list item thay vì ListTile, bạn có thể bọc tiện ích con của mình trong InkWell.
return ListView.builder(
itemBuilder: (context, index) {
return Card(
child: InkWell(
onTap: () {
print('tapped');
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: <Widget>[
column,
column,
],
),
),
),
);
},
);

Updating list data ListView trong Flutter
Adding and deleting rows in a ListView
Việc cập nhật các mục trong ListView thường là khá dễ dàng. Tất cả những gì bạn phải làm là sử dụng Stateful widget và gọi setState()
bất cứ khi nào dữ liệu thay đổi.
Vì chúng ta cần một Stateful widget, hãy thay thế cả hai BodyLayout
và _myListView()
bằng code sau:
class BodyLayout extends StatefulWidget {
@override
BodyLayoutState createState() {
return new BodyLayoutState();
}
}
class BodyLayoutState extends State<BodyLayout> {
List<String> titles = ['Sun', 'Moon', 'Star'];
@override
Widget build(BuildContext context) {
return _myListView();
}
Widget _myListView() {
return ListView.builder(
itemCount: titles.length,
itemBuilder: (context, index) {
final item = titles[index];
return Card(
child: ListTile(
title: Text(item),
onTap: () { // <-- onTap
setState(() {
titles.insert(index, 'Planet');
});
},
onLongPress: () { // <-- onLongPress
setState(() {
titles.removeAt(index);
});
},
),
);
},
);
}
}
Khi bạn nhấn vào một mục, nó sẽ thêm một mục “Planet” tại Index đó. Khi bạn nhấn và giữ một mục, nó sẽ xóa mục đó.

Kết luận
Chúng ta vừa tìm hiểu khá đầy đủ về ListView trong Flutter. Bây giờ bạn sẽ có một hiểu biết vững chắc về cách triển khai phần lớn các trường hợp sử dụng mà bạn sẽ gặp phải. Hãy like và cmt về những thắc mắc xuống phía dưới để nhận được giải đáp nhé.