Flutter: ReorderableListView

Cara menggunakan widget ReorderableListView

Yudi Setiawan
Nusanet Developers
8 min readSep 18, 2020

--

Pengenalan

Sebelum masuk ke pembahasan saya ingin bertanya terlebih dahulu. Apakah teman-teman pernah mendapatkan design atau fitur yang membuat sebuah ListView yang item-nya bisa di-draggable? Kalau pernah maka, selamat kamu akan menemukan jawabannya di tulisan ini. Saya pribadi pernah mendapatkan fitur yang mana data didalam ListView-nya bisa di-reorder atau disusun ulang dengan cara di-draggable tapi, sayangnya waktu itu kejadiannya saya belum menggunakan Flutter. Ketika itu saya masih menggunakan Android native pakai Kotlin. Seingat saya ketika itu untuk membuat fitur tersebut di Android native saya menggunakan salah satu plugin yang ada di Github. Saya lupa apa nama plugin-nya karena projek yang saya kerjakan itu source code-nya sudah tidak ada lagi.

Berulang-ulang kali saya tulis disetiap tulisan saya bahwa Flutter itu kaya akan widget-nya. Hal inilah yang membuat saya tertarik dengan Flutter. Kaya akan widget-nya membuat Flutter gampang sekali untuk mendesign tampilannya. Ditulisan ini salah satu widget yang akan saya bahas adalah widget ReorderableListView. Sesuai dengan namanya pasti teman-teman sudah bisa menebak bahwa fungsi widget ini ialah untuk melakukan reorder terhadap item-item didalam ListView. Cara widget ini untuk menyusun ulang item-item didalam ListView ialah dengan cara memberikan efek draggable terhadap item-item-nya atau dengan kata lain Si pengguna cukup tap and hold untuk memindahkan item-item-nya. Sampai di sini saya rasa diantara para pembaca mungkin ada yang bertanya seperti ini.

Apakah item-item-nya bisa di-reorder secara programmatically? Misalnya, disetiap item-nya ada button arrow up dan down yang mana ketika button ini di-tap maka, item-nya ada pindah satu tingkat keatas atau kebawah.

Jawabannya adalah bisa. Tapi, menurut saya teman-teman tidak perlu menggunakan widget ReorderableListView pun juga sudah bisa karena, untuk membuat item-item di ListView bisa dipindahkan secara programmatically keatas dan kebawah saya rasa cukup gampang. Tinggal manfaatkan saja setState dan ubah posisi datanya. Perlu diingat buat kita semua bahwa dikarenakan Flutter kaya akan widget-nya maka, kita harus benar-benar tahu kapan widget tersebut dipakai dan apa fungsi utamanya. Jangan sampai teman-teman salah menggunakan widget ya 😉.

Contoh

Untuk contoh penggunaannya saya akan membuat fitur playlist lagu. Jika teman-teman pernah pakai Spotify pasti tahulah gimana tampilan reorder playlist-nya. Kira-kira seperti inilah tampilannya di Spotify.

Tampilan reorder playlist di Spotify

Sekarang kita akan membuat fungsi yang sama seperti tampilan diatas. Untuk memulainya silakan ikuti langkah-langkah berikut ya.

1. Buat Playlist Lagu

Saya anggap teman-teman semua sudah bisa membuat projek baru di Flutter. Dan sekarang buka file main.dart dan ubah kode didalamnya menjadi seperti berikut.

import 'package:flutter/material.dart';

void main() => runApp(App());

class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomeApp(),
);
}
}

class ItemMusic {
String key;
String title;
String artist;
String album;

ItemMusic(this.key, this.title, this.artist, this.album);

@override
String toString() {
return 'ItemMusic{key: $key, title: $title, artist: $artist, album: $album}';
}
}

class HomeApp extends StatefulWidget {
@override
_HomeAppState createState() => _HomeAppState();
}

class _HomeAppState extends State<HomeApp> {
final scaffoldState = GlobalKey<ScaffoldState>();
final listMusics = <ItemMusic>[
ItemMusic('key1', 'Biarlah', 'Killing Me Inside', 'REBIRTH'),
ItemMusic('key2', 'Come As You Are', 'Nirvana', 'Nevermind (Remastered)'),
ItemMusic('key3', 'Just A Girl', 'No Doubt', 'Tragic Kingdom'),
ItemMusic('key4', 'Celebrity Skin', 'Hole', 'Celebrity Skin'),
ItemMusic('key5', 'I\'m So Free', 'Beck', 'Colors'),
ItemMusic('key6', 'Only Happy When It Rains', 'Garbage', 'Absolute Garbage'),
ItemMusic('key7', 'bad idea', 'Ariana Grande', 'thank u, next'),
ItemMusic('key8', 'So Am I', 'Ava Max', 'Som Am I'),
ItemMusic('key9', 'breathin', 'Ariana Grande', 'breathin'),
ItemMusic('key10', 'Homesick', 'Dua Lipa', 'Dua Lipa (Deluxe)'),
];

@override
Widget build(BuildContext context) {
return Scaffold(
key: scaffoldState,
appBar: AppBar(
title: Text('Liked Songs'),
actions: [
IconButton(
icon: Icon(Icons.edit),
onPressed: () {
// TODO: arahkan ke halaman edit playlist
},
tooltip: 'Edit Playlist',
),
],
),
body: ListView.separated(
padding: EdgeInsets.all(16),
itemCount: listMusics.length,
separatorBuilder: (context, index) => Divider(),
itemBuilder: (context, index) {
var itemMusic = listMusics[index];
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(itemMusic.title),
Text(
'${itemMusic.artist}${itemMusic.album}',
style: Theme.of(context).textTheme.caption,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
);
},
),
);
}
}

Berikut adalah output dari kode diatas.

Playlist Liked Songs

Pada kode diatas bisa kita lihat masih ada kode TODO yang harus kita selesaikan. TODO ini akan segera kita selesaikan pada langkah berikutnya ya.

2. Buat Edit Playlist

Masih didalam file main.dart buat satu class lagi yang bernama EditPlaylist dan kita isikan kode didalamnya menjadi seperti berikut.

class EditPlaylist extends StatefulWidget {
final List<ItemMusic> listMusics;

EditPlaylist(this.listMusics);

@override
_EditPlaylistState createState() => _EditPlaylistState();
}

class _EditPlaylistState extends State<EditPlaylist> {
final listMusicsTemp = <ItemMusic>[];

@override
void initState() {
listMusicsTemp.addAll(widget.listMusics);
super.initState();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Edit Playlist'),
actions: [
GestureDetector(
onTap: () {
// TODO: simpan playlist dan kembali ke halaman HomeApp
},
child: Text('SAVE'),
),
],
),
body: ReorderableListView(
children: listMusicsTemp.map((element) {
return Padding(
padding: EdgeInsets.all(16),
child: Row(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(element.title),
Text(
'${element.artist}${element.album}',
style: Theme.of(context).textTheme.caption,
),
],
),
Icon(Icons.drag_handle),
],
),
);
}).toList(),
onReorder: (int oldIndex, int newIndex) {
// TODO: susun ulang data collection listMucisTemp
},
),
);
}
}

Kemudian, kita ubah kode TODO: arahkan ke halaman edit playlist menjadi seperti berikut.

var result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => EditPlaylist(listMusics),
),
);
if (result != null) {
scaffoldState.currentState.showSnackBar(
SnackBar(
content: Text('Playlist updated successfully'),
),
);
setState(() {
listMusics.clear();
listMusics.addAll(result);
});
}

Kita mau agar ketika playlist-nya berubah maka, kita bisa meng-update list yang dihalaman home. Oleh karena itu, kita menggunakan await ketika pindah halaman ke EditPlaylist dan cek nilai result . Jika nilai result tidak sama dengan null maka, kita reset collection listMusics dan isi ulang dengan data yang terbaru.

Sekarang coba kita jalankan lagi programnya.

Wah, red screen.

Kok bisa error ya? 🤔. Mari kita baca pesan error-nya. Dia bilang bahwa All children of this widget must have a key. Ouh, ternyata dia bilang kalau semua widget yang ada didalam widget ReorderableListView harus memiliki key-nya masing-masing. Ok, sekarang yang perlu kita lakukan adalah tambahkan key didalam widget Padding seperti berikut.

Padding(
key: Key(element.key), // ini ya
padding: EdgeInsets.all(16),
child: Row(
children: [
...

Sekarang coba jalankan ulang programnya.

Hm, kurang rapi ya

Coba kita rapikan ya tampilannya agar icon draggable itu berada di posisi paling kanan. Kita tambahkan property mainAxisAlignment: MainAxisAlignment.spaceBetween didalam widget Row seperti berikut.

return Padding(
key: Key(element.key),
padding: EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, // ini ya
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
...

Sekarang coba kita jalankan lagi programnya.

Cakep… Sudah rapi ya

Sampai disini list-nya sudah tampil dan bisa digeser-geser hanya saja selesai digeser datanya akan kembali ke semula. Ini dikarenakan kita belum tambahkan logic didalam property onReorder . Oya, untuk menggeser-gesernya pakai mode tap and hold ya pada teks atau icon draggable-nya.

Sekarang kita lihat bahwa didalam property onReorder terdapat 2 parameter yaitu oldIndex dan newIndex . Jadi, property ini akan terpanggil ketika kita selesai melakukan reorder listview-nya. Dan oleh sebab itu, didalamnya ada 2 parameter tadi oldIndex dan newIndex . Sebelum tambahkan logic-nya coba kita debugPrint nilai oldIndex dan newIndex .

onReorder: (int oldIndex, int newIndex) {
debugPrint('oldIndex: $oldIndex & newIndex: $newIndex');
// TODO: susun ulang data collection listMucisTemp
},

Untuk tes pertama, coba kita geser lagu Just A Girl ke posisi dibawah lagu bad idea.

Tes pertama

Dari tes pertama, kita mendapatkan hasil console log seperti ini.

oldIndex: 2 & newIndex: 7

oldIndex bernilai 2 dan newIndex bernilai 7. Sekarang coba kita hitung ya index-nya apakah sudah benar atau belum

Perhitungan index pada tes pertama

Hm… 🤔 kok aneh ya. Di console log tadi bahwa oldIndex bernilai 2 dan newIndex bernilai 7. Tapi, pada gambar (sebelah kanan) diatas kita lihat bahwa pada saat dipindah itu posisinya berada di index ke 6. Apa mungkin newIndex itu bukan index tapi, nilai urutannya. Kalau dihitung berdasarkan posisi urutannya maka, newIndex pas nilainya 7. Tapi, kayaknya nggak mungkin deh. Hm… coba kita baca kode dokumentasinya ya.

Kode dokumentasi

Oh, gitu ternyata. Kalau dari snippet kode didokumentasinya berarti, perhitungan kita tadi sudah pas. Pada snippet kode diatas bisa kita lihat bahwa ada pengkondisian seperti berikut.

if (oldIndex < newIndex) {
// removing the item at oldIndex will shorten the list by 1.
newIndex -= 1;
}

Berarti, ketika nilai oldIndex lebih kecil dari newIndex maka, nilai newIndex dikurangin 1. Jadi, pengkondisian ini akan terpenuhi jika kita menggeser sebuah item yang nilai index awalnya lebih kecil dari index barunya. Berarti, tes pertama tadi sudah pas. Nilai newIndex -nya lebih besar dari oldIndex dan oleh karena itu nilai newIndex -nya 7 yang mana nantinya nilai newIndex -nya akan kita kurangin dengan 1 dan hasilnya newIndex bernilai 6. Gimana? Masih pahamkan? Kalau tidak paham nggak apa-apa silakan lanjutkan saja membacanya. Saya yakin nanti teman-teman bakalan paham setelah saya tulisan kode logic-nya.

Kita tes untuk case kedua ya. Sekarang kita akan tes geser lagu Just A Girl ke posisi paling atas.

Perhitungan index pada tes kedua

Berikut adalah output console log-nya pada tes kedua.

oldIndex: 2 & newIndex: 0

Berdasarkan info hasil console log dan gambar pada tes kedua berarti, kode snippet yang dari dokumentasinya tadi benar. Sekarang kita sudah dapat rumusnya nih. Rumusnya adalah sebagai berikut.

if (oldIndex < newIndex) {
// removing the item at oldIndex will shorten the list by 1.
newIndex -= 1;
}

Sekarang kita tinggal ubah kode TODO: susun ulang data collection listMusicTemp menjadi seperti berikut.

onReorder: (int oldIndex, int newIndex) {
debugPrint('oldIndex: $oldIndex & newIndex: $newIndex');
if (oldIndex < newIndex) {
newIndex -= 1;
}
setState(() {
var itemMusicTemp = listMusicsTemp[oldIndex];
listMusicsTemp.removeAt(oldIndex);
listMusicsTemp.insert(newIndex, itemMusicTemp);
});
}

Sekarang coba jalankan lagi programnya.

Hasil akhir Edit Playlist

3. Simpan Perubahan Playlist

Nah, langkah terakhirnya adalah kita tinggal kirim perubahan playlist-nya ke halaman home. Eh, btw itu tulisan SAVE -nya yang di AppBar kurang rapi ya. Kita ganti menjadi icon Save ya. Sekarang mari kita ubah didalam property actions yang didalam widget AppBar menjadi seperti berikut.

appBar: AppBar(
title: Text('Edit Playlist'),
actions: [
IconButton(
icon: Icon(Icons.save),
onPressed: () {
Navigator.pop(context, listMusicsTemp);
},
)
],

),

Sekarang coba kita jalankan lagi programnya.

Hasil Akhir

Kesimpulan

Kesimpulannya adalah bahwa penggunaan widget ReorderableListView termasuklah mudah dan kita bahkan tidak perlu menggunakan plugin tambahan untuk membuatnya. Dan jangan lupa pesan saya untuk kita semua adalah bahwa Flutter kaya akan widgetnya oleh karena itu, kita harus tahu kapan menggunakannya dan apa fungsi utama setiap widget yang ada. Untuk kode lengkapnya pada tulisan ini bisa dilihat di Github ya.

--

--