'How to display a dynamic map using Tabs and SliverGrid

I have a Model called service provider that contains a the following data:

class ServiceProvider {
    final String logoUrl;
    final int lowerPrice;
    final String name;
    final int numberOfOrders;
    final int onTime;
    final int ordersPercentage;
    final int upperPrice;
    final double rating;
    final Map<dynamic,dynamic> albums;
  };
}

I am able to parse all the data correctly without any problems however I hit a brick wall when I tried to display the albums map basically its a map of strings with lists as values looking like this :

 albums: {
            "Nature" :[
              "imageURLHere",
              "imageURLHere",
              "imageURLHere"
            ],
            "Wedding" : [
               "imageURLHere"
             ],
            "Portraits" : [
              "imageURLHere",
              "imageURLHere"
            ],
          }

My idea was to use a Tabs to represent each Key (Nature,wedding,portrait) and use a SliverGrid to display the images.

I used a Custom Tab Builder to be able to build the TabBar and TabViews Dynamically, however I cannot think of a way to make the SliverGrid Inside to display each image list.

basically I can only show one of the lists 3 times.

keeping in mind that the number of albums and number of images is variable.

This is the custom tab Builder that I am using

  final int itemCount;
  final IndexedWidgetBuilder tabBuilder;
  final IndexedWidgetBuilder pageBuilder;
  final Widget? stub;
  final ValueChanged<int>? onPositionChange;
  final ValueChanged<double>? onScroll;
  final int? initPosition;

  CustomTabView({
    required this.itemCount,
    required this.tabBuilder,
    required this.pageBuilder,
    this.stub,
    this.onPositionChange,
    this.onScroll,
    this.initPosition,
  });

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

class _CustomTabsState extends State<CustomTabView> with TickerProviderStateMixin {
  TabController? controller;
  int? _currentCount;
  int? _currentPosition;

  @override
  void initState() {
    _currentPosition = widget.initPosition ?? 0;
    controller = TabController(
      length: widget.itemCount,
      vsync: this,
      initialIndex: _currentPosition!,
    );
    controller!.addListener(onPositionChange);
    controller!.animation!.addListener(onScroll);
    _currentCount = widget.itemCount;
    super.initState();
  }

  @override
  void didUpdateWidget(CustomTabView oldWidget) {
    if (_currentCount != widget.itemCount) {
      controller!.animation!.removeListener(onScroll);
      controller!.removeListener(onPositionChange);
      controller!.dispose();

      if (widget.initPosition != null) {
        _currentPosition = widget.initPosition;
      }

      if (_currentPosition! > widget.itemCount - 1) {
        _currentPosition = widget.itemCount - 1;
        _currentPosition = _currentPosition! < 0 ? 0 :
        _currentPosition;
        if (widget.onPositionChange is ValueChanged<int>) {
          WidgetsBinding.instance!.addPostFrameCallback((_){
            if(mounted) {
              widget.onPositionChange!(_currentPosition!);
            }
          });
        }
      }

      _currentCount = widget.itemCount;
      setState(() {
        controller = TabController(
          length: widget.itemCount,
          vsync: this,
          initialIndex: _currentPosition!,
        );
        controller!.addListener(onPositionChange);
        controller!.animation!.addListener(onScroll);
      });
    } else if (widget.initPosition != null) {
      controller!.animateTo(widget.initPosition!);
    }

    super.didUpdateWidget(oldWidget);
  }

  @override
  void dispose() {
    controller!.animation!.removeListener(onScroll);
    controller!.removeListener(onPositionChange);
    controller!.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    if (widget.itemCount < 1) return widget.stub ?? Container();

    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: <Widget>[
        Container(
          alignment: Alignment.center,
          child: TabBar(
            isScrollable: true,
            controller: controller,
            labelColor: Theme.of(context).primaryColor,
            unselectedLabelColor: Theme.of(context).hintColor,
            indicator: BoxDecoration(
              border: Border(
                bottom: BorderSide(
                  color: Theme.of(context).primaryColor,
                  width: 2,
                ),
              ),
            ),
            tabs: List.generate(
              widget.itemCount,
                  (index) => widget.tabBuilder(context, index),
            ),
          ),
        ),
        Expanded(
          child: TabBarView(
            controller: controller,
            children: List.generate(
              widget.itemCount,
                  (index) => widget.pageBuilder(context, index),
            ),
          ),
        ),
      ],
    );
  }

  onPositionChange() {
    if (!controller!.indexIsChanging) {
      _currentPosition = controller!.index;
      if (widget.onPositionChange is ValueChanged<int>) {
        widget.onPositionChange!(_currentPosition!);
      }
    }
  }

  onScroll() {
    if (widget.onScroll is ValueChanged<double>) {
      widget.onScroll!(controller!.animation!.value);
    }
  }
}

and this is where I use it and build my SliverGrid inside it

class IndividualServiceProviderScreen extends StatelessWidget {
  final ServiceProvider serviceProvider;

  const IndividualServiceProviderScreen({
    required this.serviceProvider,
  });

  @override
  Widget build(BuildContext context) {
    List data = serviceProvider.albums.keys.toList();
    int currentPosition = 1;

    return DefaultTabController(
        length: serviceProvider.albums.keys.length,
        child: Scaffold(
            appBar: AppBar(
              title: Text(serviceProvider.name),
            ),
            body: NestedScrollView(
              physics: BouncingScrollPhysics(
                  parent: AlwaysScrollableScrollPhysics()),
              headerSliverBuilder:
                  (BuildContext context, bool innerBoxIsScrolled) {
                return <Widget>[
                  SliverToBoxAdapter(
                    child: ServiceProviderDetailsCard(
                      serviceProvider: serviceProvider,
                    ),
                  ),
                ];
              },
              body: CustomTabView(
                initPosition: currentPosition,
                itemCount: data.length,
                tabBuilder: (context, index) => Tab(text: data[index]),
                pageBuilder: (context, index) =>
                    CustomScrollView(
                      physics: BouncingScrollPhysics(
                      parent: AlwaysScrollableScrollPhysics()),
                      slivers: [
                      SliverGrid(
                        gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
                        maxCrossAxisExtent: 200,
                        mainAxisSpacing: 10.0,
                        crossAxisSpacing: 10.0,
                        childAspectRatio: 1.0,
                      ),
                      delegate: SliverChildBuilderDelegate(
                        (BuildContext context, int index) {
// I want to be able to use the CustomTabBuilder index inside 
//here but i can't pass the index inside because 
//it gets overridden by the index of the sliver builder
                          if (index < serviceProvider.albums[data[0]].length) { 
                            return Image(
                                image: NetworkImage(
                                    serviceProvider.albums[data[0]][index]));
                          }
                        },
                        childCount: serviceProvider.albums[data[0]].length,
                      ),
                    )
                  ],
                ),
                onPositionChange: (index) {
                  print('current position: $index');
                  currentPosition = index;
                },
              ),
            ),
            bottomNavigationBar: Container(
              decoration: BoxDecoration(
                color: Colors.white,
                borderRadius: BorderRadius.only(
                    topRight: Radius.circular(10),
                    topLeft: Radius.circular(10)),
                boxShadow: [
                  BoxShadow(
                      color: Colors.black38, spreadRadius: 0, blurRadius: 10),
                ],
              ),
              child: ClipRRect(
                borderRadius: BorderRadius.only(
                  topLeft: Radius.circular(10.0),
                  topRight: Radius.circular(10.0),
                ),
                child: Padding(
                  padding: const EdgeInsets.fromLTRB(50, 8, 50, 8),
                  child: ElevatedButton(
                      style:
                          ElevatedButton.styleFrom(minimumSize: Size(20, 50)),
                      onPressed: () => {print("Booking")},
                      child: Text(
                        "Book now",
                        style: TextStyle(fontSize: 24),
                      )),
                ),
              ),
            )
        )
    );
  }
}

I tried using the onPositionChange function to pass the current index of the TabView inside the SliverGrid's delegate, however that still doesn't work because the images are loaded after the change happens and that is not the correct behavior

This is what I managed to build so far

But as mentioned above whenever I change tabs same images are displayed, and when I use currentIndex the images do change but to the previous index not the actually pressed tab.



Solution 1:[1]

I managed to solve it , the solution was actually pretty trivial. I am surprised I didn't think of it earlier I created a stateless widget that takes a list of image Urls and inside it I put my entire custom scroll view and I used the page builder of the CustomTabViewer to pass each list using the index like so

 CustomTabView(
                initPosition: currentPosition,
                itemCount: keys.length,
                tabBuilder: (context, index) => Tab(text: keys[index]),
                pageBuilder: (context, index) => ImagesView(
                    imageUrls:
                        serviceProvider.albums[keys[index]] as List<dynamic>),
                onPositionChange: (index) {
                  currentPosition = index;
                },
              ),
            ),

and the ImagesView Implemented as follows

import 'package:flutter/material.dart';
class ImagesView extends StatelessWidget {

  final List<dynamic> imageUrls;

  const ImagesView({
    required this.imageUrls,
  });

  @override
  Widget build(BuildContext context) {
    return CustomScrollView(
      physics: BouncingScrollPhysics(
          parent: AlwaysScrollableScrollPhysics()),
      slivers: [
        SliverGrid(
          gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
            maxCrossAxisExtent: 200,
            mainAxisSpacing: 10.0,
            crossAxisSpacing: 10.0,
            childAspectRatio: 1.0,
          ),
          delegate: SliverChildBuilderDelegate(
                (BuildContext context, int index) {
              if (index < imageUrls.length) {
                return Image(
                    image: NetworkImage(
                        imageUrls[index]));
              }
            },
            childCount: imageUrls.length,
          ),
        )
      ],
    );
  }
}

The Intended behavior was as follows

Scrolling through tabs and nested slivr Grid

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Mahmoud Gamal Eid