Nusanet Developers
Published in

Nusanet Developers

Flutter Infinite ListView

How to implement infinite ListView in Flutter

Pengenalan

Sebuah aplikasi pada umumnya pasti terdapat fitur untuk menampilkan sekumpulan data. Data-data tersebut biasanya ditampilkan sesuai dengan platform-nya masing-masing. Kalau di desktop/web umumnya menggunakan table pagination.

https://mdbootstrap.com/docs/b4/jquery/tables/pagination/

Lalu, untuk platform mobile biasanya kita menampilkan datanya dalam bentuk list.

https://dribbble.com/shots/10181589-Order-List-and-Track-Orders-Ui-Design

Untuk mobile, implementasi pagination-nya kita bisa menggunakan teknik Infinite Scrolling atau Infinite ListView. Jadi, konsepnya itu adalah app-nya menampilkan list datanya dalam bentuk ListView lalu, ketika berada di paling bawah maka, app-nya akan memuat lagi ke API atau database untuk menampilkan data berikutnya.

Cara Pembuatan

Langkah pertamanya, silakan kita buat projek baru dengan nama flutter-swapi.

Buat projek baru

Untuk membuatnya ada beberapa plugin yang perlu kita tambahkan kedalam projek kita. Silakan buka file pubspec.yaml dan tambahkan beberapa plugin berikut.

  1. dio
  2. json_annotation
  3. json_serializable
  4. build_runner

Catatan: Untuk versinya silakan pakai yang versi terbaru ya.

...
dependencies:
flutter:
sdk: flutter

cupertino_icons: ^1.0.2
dio: ^4.0.4
json_annotation: ^4.4.0

dev_dependencies:
flutter_test:
sdk: flutter

flutter_lints: ^1.0.0
json_serializable: ^6.1.4
build_runner: ^2.1.7
...

Langkah berikutnya, kita perlu membuat class model dari respon endpoint yang bakalan kita pakai. Berikut adalah endpoint yang akan kita pakai.

https://swapi.dev/api/people?page=:page

Dan berikut adalah format respon dari endpoint tersebut.

{
"count": 82,
"next": "https://swapi.dev/api/people/?page=2",
"previous": null,
"results": [
{
"name": "Luke Skywalker",
"height": "172",
"mass": "77",
"hair_color": "blond",
"skin_color": "fair",
"eye_color": "blue",
"birth_year": "19BBY",
"gender": "male",
"homeworld": "https://swapi.dev/api/planets/1/",
"films": [
"https://swapi.dev/api/films/1/",
"https://swapi.dev/api/films/2/",
"https://swapi.dev/api/films/3/",
"https://swapi.dev/api/films/6/"
],
"species": [],
"vehicles": [
"https://swapi.dev/api/vehicles/14/",
"https://swapi.dev/api/vehicles/30/"
],
"starships": [
"https://swapi.dev/api/starships/12/",
"https://swapi.dev/api/starships/22/"
],
"created": "2014-12-09T13:50:51.644000Z",
"edited": "2014-12-20T21:17:56.891000Z",
"url": "https://swapi.dev/api/people/1/"
},
...
]
}

Nah, karena kita sudah tahu format respon JSON-nya. Maka, sekarang kita buat class modelnya. Caranya silakan buat file baru dengan nama people_response.dart. Dan isikan dengan kode berikut.

import 'package:json_annotation/json_annotation.dart';

part 'people_response.g.dart';

@JsonSerializable()
class PeopleResponse {
@JsonKey(name: 'count')
final int count;
@JsonKey(name: 'next')
final String? nextUrl;
@JsonKey(name: 'previous')
final String? previousUrl;
@JsonKey(name: 'results')
final List<ItemPeopleResponse> results;

PeopleResponse({
required this.count,
required this.nextUrl,
required this.previousUrl,
required this.results,
});

factory PeopleResponse.fromJson(Map<String, dynamic> json) => _$PeopleResponseFromJson(json);

Map<String, dynamic> toJson() => _$PeopleResponseToJson(this);
}

@JsonSerializable()
class ItemPeopleResponse {
@JsonKey(name: 'name')
final String? name;
@JsonKey(name: 'birth_year')
final String? birthYear;
@JsonKey(name: 'gender')
final String? gender;

ItemPeopleResponse({
required this.name,
required this.birthYear,
required this.gender,
});

factory ItemPeopleResponse.fromJson(Map<String, dynamic> json) => _$ItemPeopleResponseFromJson(json);

Map<String, dynamic> toJson() => _$ItemPeopleResponseToJson(this);
}

Untuk menghilangkan tanda error-nya. Kita perlu jalankan build runner-nya untuk membuat file generator-nya. Untuk membuat file generator-nya silakan jalankan perintah berikut.

flutter pub run build_runner build

Langkah berikutnya kita perlu membuat tampilannya. Silakan buka file main.dart dan ubah kode didalamnya menjadi seperti berikut.

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_swapi/people_response.dart';

enum ItemType {
data,
loading,
}

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Swapi',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Swapi'),
);
}
}

class MyHomePage extends StatefulWidget {
final String title;

const MyHomePage({Key? key, required this.title}) : super(key: key);

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
final _scrollController = ScrollController();
final _listData = <Data>[];
final _dio = Dio();

String? _nextUrl;
var _isLoading = false;
var _paddingBottom = 0.0;
var _errorMessage = '';

@override
void initState() {
_doLoadData();
// TODO: Buat fitur load more
super.initState();
}

@override
void dispose() {
_scrollController.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
final mediaQueryData = MediaQuery.of(context);
_paddingBottom = mediaQueryData.padding.bottom;
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: _buildWidgetBody(),
);
}

Widget _buildWidgetBody() {
if (_errorMessage.isNotEmpty) {
return _buildWidgetInfoTryAgain(_errorMessage);
}

if (_listData.isEmpty) {
if (_isLoading) {
return const Center(
child: CircularProgressIndicator.adaptive(),
);
} else {
return _buildWidgetInfoTryAgain('Data not available');
}
} else {
return ListView.separated(
padding: EdgeInsets.only(
left: 16,
top: 16,
right: 16,
bottom: _paddingBottom > 0 ? _paddingBottom : 16,
),
itemBuilder: (context, index) {
final itemPeople = _listData[index];
final typeData = itemPeople.itemType;
if (typeData == ItemType.loading) {
return const Center(
child: CircularProgressIndicator.adaptive(),
);
} else if (typeData == ItemType.data) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
itemPeople.people?.name ?? '-',
style: Theme.of(context).textTheme.headline6,
),
Text(
itemPeople.people?.gender ?? '-',
style: Theme.of(context).textTheme.bodyText2,
),
Text(
itemPeople.people?.birthYear ?? '-',
style: Theme.of(context).textTheme.caption,
),
],
),
),
);
} else {
return Container();
}
},
separatorBuilder: (context, index) => Container(
height: 8,
),
itemCount: _listData.length,
);
}
}

Widget _buildWidgetInfoTryAgain(String text) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(text),
ElevatedButton(
child: const Text('Try Again'),
onPressed: () {
_doLoadData();
},
),
],
),
);
}

Future<void> _doLoadData() async {
_errorMessage = '';
var url = '';
if (_listData.isEmpty) {
url = 'https://swapi.dev/api/people?page=1';
} else {
url = _nextUrl ?? '';
}
if (url.isEmpty) {
return;
}
setState(() => _isLoading = true);
try {
final response = await _dio.get(url);
if (response.statusCode.toString().startsWith('2')) {
final peopleResponse = PeopleResponse.fromJson(response.data);
if (_listData.isNotEmpty) {
/// Hapus widget elemen loading
final lastItem = _listData.last;
if (lastItem.itemType == ItemType.loading) {
_listData.removeLast();
}
}

/// Masukkan data baru kedalam _listData
_listData.addAll(
peopleResponse.results.map(
(element) {
return Data(
people: element,
itemType: ItemType.data,
);
},
),
);

/// Jika nilai _nextUrl != null maka, tambahkan widget elemen loading
_nextUrl = peopleResponse.nextUrl;
if (_nextUrl != null) {
_listData.add(
Data(
people: null,
itemType: ItemType.loading,
),
);
}
} else {
/// Munculkan pesan error
_listData.clear();
_errorMessage = "Failed to get data";
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(_errorMessage)));
}
} catch (error) {
/// Munculkan pesan error
_errorMessage = 'Failed to get data';
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(_errorMessage)));
} finally {
setState(() => _isLoading = false);
}
}
}

class Data {
final ItemPeopleResponse? people;
final ItemType itemType;

Data({
required this.people,
required this.itemType,
});
}

Adapun output dari kode UI diatas adalah sebagai berikut.

Di langkah sebelumnya, kita sudah berhasil menampilkan datanya dari endpoint. Tapi, fitur infinite list-nya belum bisa berfungsi. Nah, di langkah ini kita akan membuatnya. Silakan buka kembali file main.dart dan ubah kode berikut.

// TODO: Buat fitur load more

Menjadi seperti berikut.

_scrollController.addListener(() {
if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
_doLoadData();
}
});

Kemudian, kita set property controller dari widget ListView.separated .

return ListView.separated(
controller: _scrollController,
padding: EdgeInsets.only(
left: 16,

Berikut adalah video hasilnya.

Fitur infinite ListView

Jadi, di tulisan ini kita telah berhasil membuat Infinite ListView di Flutter. Caranya cukup mudah kan. Daripada kita pakai plugin spesial untuk fitur tersebut mending kita buat sendiri dan lebih fully customize. Untuk source code lengkapnya bisa dilihat di Github berikut ya.

--

--

Stories and insights from the developers in Nusanet

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store