Custom Flutter GroupListView Widget
George Ikwegbu Chinedu
Posted on February 5, 2024
Table of Content
- 🎉 Introduction
- ✨ Items JSON
- ⚙️ Modeling our Data
- 🍦 Implementation
- 💪 Full Code
- 💙 Summary
🎉 Introduction
You may need to arrange items in an orderly manner at some point. These objects might be anything, employing a specific property in their object, from a list of books to questions.
NB: Check out my previous article, in which I explain how to create a form in Flutter dynamically. I used this article to group the questions according to their order.
✨ Items JSON
I would be constructing a basic JSON data set as our source data in order to keep things simple. Although this will be relatively little data, you can reuse the function in your larger dataset after you understand how it operates.
// Continent and Country Json data
const itemsJsonData =
[
{
"id": 0,
"continent": "Africa",
"country": "Nigeria",
"order": 1
},
{
"id": 1,
"continent": "Asia",
"country": "Cambodia",
"order": 10
},
{
"id": 2,
"continent": "NorthAmerica",
"country": "Canada",
"order": 2
},
{
"id": 3,
"continent": "NorthAmerica",
"order": 2
},
{
"id": 4,
"continent": "SouthAmerica",
"country": "Brazil",
"order": 3
},
{
"id": 5,
"continent": "Europe",
"country": "Austria",
"order": 12
},
{
"id": 6,
"continent": "Africa",
"country": "Burundi",
"order": 1
},
{
"id": 7,
"continent": "Asia",
"country": "Azerbaijan",
"order": 10
},
{
"id": 8,
"continent": "NorthAmerica",
"country": "Belize",
"order": 2
},
{
"id": 9,
"continent": "NorthAmerica",
"country": "Antigua and Barbuda",
"order": 2
},
{
"id": 10,
"continent": "SouthAmerica",
"country": "Argentina",
"order": 3
},
{
"id": 11,
"continent": "Europe",
"country": "Andorra",
"order": 12
},
];
NB: Pay close attention to the continent and country
dataset and it's ordering and how it is an `int`..
NB: Request the server sends the `order` as an `int`
value. Else, you might have to map over the keys and turn
them to `int`
⚙️ Modeling our Data
For simplicity sake, we would be creating a model, which will enable us interact with the data efficiently.
class ContCountryModel {
final int? id;
final String? continent;
final String? country;
final int? order;
ContCountryModel({
this.id,
this.continent,
this.country,
this.order,
});
factory ContCountryModel.fromJson(Map<String, dynamic> json) => ContCountryModel(
id: json["id"],
continent: json["continent"],
country: json["country"],
order: json["order"],
);
Map<String, dynamic> toJson() => {
"id": id,
"continent": continent,
"country": country,
"order": order,
};
List<ContCountryModel> fromList(List<dynamic> items) {
if(items.isEmpty) {
return [];
} else {
return items.map((e) => ContCountryModel.fromJson(e)).toList();
}
}
}
🍦 Implementation
NB: We won't be covering the network calls; as I assume, you already have the data list in your system and just want to create the grouped list
// We will be needing the `groupBy` found in the `collection` package to first group our items.
import 'package:collection/collection.dart';
// Placed within the State of your stateful widget
List<ContCountryModel> _itemList = [];
Map groupItemsByOrder(List<ContCountryModel> items) {
return groupBy(items, (item) => item.order);
}
// When the above function runs, it'll arrange all the items by their `order`. But since the `order` is a `String` datatype, it won't be mathematically correct, e.g; `10` will come before `2`.
// Placed within state builder:
Map groupedItems = groupItemsByOrder(_itemList);
// NB: You can either load the `_itemList` in the `initState` or use any `state management` system of your choice to get the data.
// Here we will be using a `ListView.builder` within a `ListView.builder` to get our `Continents` and the `Countries` within it in a grouped fashion
ListView.builder(
itemCount: groupedItems.length,
itemBuilder: (BuildContext context, int index) {
var sortedKeys = groupedItems.keys.toList()..sort();
// Since the groupedItems is a Map,we can use the .keys,
to get all the keys, then turn it into a list before
sorting it
int order = sortedKeys.elementAt(index);
// Recall that groupedItems returns a Map of data grouped
by 'order', hence using the above listView index,
// we fetch the key that matches the index, which inturn
becomes the active 'order' in the current loop.
List<ContCountryModel> itemsInCategory =
groupedItems[order]!;
// With the help of the retrieved `order` , we now get
the items in the category data, with which we would
use to create the second ListView
// Return a widget representing the category and its
items
return Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 20),
child: Text(
"${itemsInCategory.first.continent}:"),
),
ListView.builder(
shrinkWrap: true,
physics: const ClampingScrollPhysics(),
itemCount: itemsInCategory.length,
itemBuilder: (BuildContext context, int
index) {
ContCountryModel each =
itemsInCategory[index];
int _originalListIndex = _itemList
.indexWhere((q) => q.id == each.id);
// Return a widget representing the item
return Padding(
padding: const EdgeInsets.symmetric(
vertical: 10.0,
),
child: Text("${each.country}"),
);
},
),
],
);
},
),
💪 Full Code
Here is the full code implementation:
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'GrouupedList Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Flutter Grouped List'),
);
}
}
class MyHomePage extends StatefulWidget {
final String title;
const MyHomePage({
super.key,
required this.title,
}) : super();
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
// Placed within the State of your stateful widget
List<ContCountryModel> _itemList = [];
Map groupItemsByOrder(List<ContCountryModel> items) {
return groupBy(items, (item) => item.order);
}
// When the above function runs, it'll arrange all the items by their `order`. But since the `order` is a `String` datatype, it won't be mathematically correct, e.g; `10` will come before `2`.
@override
void initState() {
_itemList = ContCountryModel().fromList(itemsJsonData);
super.initState();
}
@override
Widget build(BuildContext context) {
Map groupedItems = groupItemsByOrder(_itemList);
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Container(
height: double.infinity,
width: double.infinity,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: ListView.builder(
itemCount: groupedItems.length,
itemBuilder: (BuildContext context, int index) {
var sortedKeys = groupedItems.keys.toList()..sort();
// Since the groupedItems is a Map,we can use the .keys, to get all the keys, then turn it into a list before sorting it
int order = sortedKeys.elementAt(index);
// Recall that groupedItems returns a Map of data grouped by 'order', hence using the above listView index,
// we fetch the key that matches the index, which inturn becomes the active 'order' in the current loop.
List<ContCountryModel> itemsInCategory =
groupedItems[order]!;
// With the help of the retrieved `order` , we now get the items in the category data, with which we would use to create the second ListView
// Return a widget representing the category and its items
return Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 20),
child: Text(
"${itemsInCategory.first.continent}:"
),
),
ListView.builder(
shrinkWrap: true,
physics: const ClampingScrollPhysics(),
itemCount: itemsInCategory.length,
itemBuilder:
(BuildContext context, int index) {
ContCountryModel each = itemsInCategory[index];
int _originalListIndex = _itemList
.indexWhere((q) => q.id == each.id);
// Return a widget representing the item
return Padding(
padding: const EdgeInsets.symmetric(
vertical: 10.0,
),
child: Text("${each.country}"),
);
},
),
],
);
},
), // ListView Ends here
)
],
),
)
// Body ends here
);
}
}
class ContCountryModel {
final int? id;
final String? continent;
final String? country;
final int? order;
ContCountryModel({
this.id,
this.continent,
this.country,
this.order,
});
factory ContCountryModel.fromJson(Map<String, dynamic> json) => ContCountryModel(
id: json["id"],
continent: json["continent"],
country: json["country"],
order: json["order"],
);
Map<String, dynamic> toJson() => {
"id": id,
"continent": continent,
"country": country,
"order": order,
};
List<ContCountryModel> fromList(List<dynamic> items) {
if(items.isEmpty) {
return [];
} else {
return items.map((e) => ContCountryModel.fromJson(e)).toList();
}
}
}
// Continent and Country Json data
const itemsJsonData =
[
{
"id": 0,
"continent": "Africa",
"country": "Nigeria",
"order": 1
},
{
"id": 1,
"continent": "Asia",
"country": "Cambodia",
"order": 10
},
{
"id": 2,
"continent": "NorthAmerica",
"country": "Canada",
"order": 2
},
{
"id": 3,
"continent": "NorthAmerica",
"order": 2
},
{
"id": 4,
"continent": "SouthAmerica",
"country": "Brazil",
"order": 3
},
{
"id": 5,
"continent": "Europe",
"country": "Austria",
"order": 12
},
{
"id": 6,
"continent": "Africa",
"country": "Burundi",
"order": 1
},
{
"id": 7,
"continent": "Asia",
"country": "Azerbaijan",
"order": 10
},
{
"id": 8,
"continent": "NorthAmerica",
"country": "Belize",
"order": 2
},
{
"id": 9,
"continent": "NorthAmerica",
"country": "Antigua and Barbuda",
"order": 2
},
{
"id": 10,
"continent": "SouthAmerica",
"country": "Argentina",
"order": 3
},
{
"id": 11,
"continent": "Europe",
"country": "Andorra",
"order": 12
},
];
💙 Summary
In this article, we were able to
create a
model
class for our item that also houses a.fromList
method that enables you to easily get a list of items from adynamic
list and the.fromJson
also to easily map our json data onto our model.Provide a dummy list for the item, and lastly,
Implement a
ListView
within aListView
, which would represent the sorted items while having it's continent as a Header placed above each section of the list..
NB: Once you understand how the above was achieved, you can create multiple UI using the same approach.
Posted on February 5, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.