Custom Flutter GroupListView Widget

gikwegbu

George Ikwegbu Chinedu

Posted on February 5, 2024

Custom Flutter GroupListView Widget

Table of Content

🎉 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
  },

];


Enter fullscreen mode Exit fullscreen mode
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`
Enter fullscreen mode Exit fullscreen mode

⚙️ 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();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

🍦 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}"),
      );
     },
    ),
   ],
  );
 },
),

Enter fullscreen mode Exit fullscreen mode

💪 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
  },

];


Enter fullscreen mode Exit fullscreen mode

💙 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 a dynamic 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 a ListView, 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.

💖 💪 🙅 🚩
gikwegbu
George Ikwegbu Chinedu

Posted on February 5, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Custom Flutter GroupListView Widget
flutter Custom Flutter GroupListView Widget

February 5, 2024