skip to Main Content

How to implement a ListView with alphabet shown as sticky side headers, similarly to the Telegram application?

enter image description here

For example, while we look at the countries that begin with the letter "E", the letter "E" is not scrolling as long as there are countries that begin with the letter "F". Then the letter "E" is changed to the letter "F".

2

Answers


  1. Here is a solution using Nested ListViews.

    enter image description here

    Interesting points about the solution:

    1. I used the diacritic package to remove the diacritics when indexing the list of Persons. You wouldn’t want to exclude all of Norway’s Øyvind, would you?
    2. I used a SplayTreeMap to keep the Map sorted.
    3. I used Flutter Hooks for the ScrollController and the Persons mapping, happening only when contacts is changed.

    Full source code

    import 'dart:collection';
    
    import 'package:diacritic/diacritic.dart';
    import 'package:faker/faker.dart';
    import 'package:flutter/material.dart';
    import 'package:flutter_hooks/flutter_hooks.dart';
    import 'package:freezed_annotation/freezed_annotation.dart';
    
    part 'my_file.freezed.dart'; // File generated by freezed
    
    void main() {
      runApp(
        MaterialApp(
          debugShowCheckedModeBanner: false,
          title: 'Flutter Demo',
          home: DirectoryPage(contacts: dummyData),
        ),
      );
    }
    
    class DirectoryPage extends HookWidget {
      final List<Person> contacts;
    
      const DirectoryPage({Key key, this.contacts}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        final _scrollController = useScrollController();
        final _mappedContacts = useState<SplayTreeMap<String, List<Person>>>(null);
        useEffect(() {
          _mappedContacts.value = contacts.fold<SplayTreeMap<String, List<Person>>>(
            SplayTreeMap<String, List<Person>>(),
            (acc, curr) {
              final firstChar = removeDiacritics(curr.name)[0];
              acc..[firstChar] ??= [];
              return acc..[firstChar].add(curr);
            },
          );
          return;
        }, [contacts]);
        return Scaffold(
          body: Scrollbar(
            controller: _scrollController,
            isAlwaysShown: true,
            child: ListView.separated(
              controller: _scrollController,
              itemBuilder: (context, index) {
                final key = _mappedContacts.value.keys.elementAt(index);
                return Row(
                  mainAxisSize: MainAxisSize.min,
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: Text(key, style: TextStyle(fontSize: 36)),
                    ),
                    const SizedBox(width: 8.0),
                    Expanded(
                      child: ListView(
                        shrinkWrap: true,
                        children: _mappedContacts.value[key]
                            .map((person) => PersonTile(person: person))
                            .toList(),
                      ),
                    ),
                  ],
                );
              },
              separatorBuilder: (_, __) => Divider(),
              itemCount: _mappedContacts.value.length,
            ),
          ),
        );
      }
    }
    
    class PersonTile extends StatelessWidget {
      final Person person;
    
      const PersonTile({Key key, this.person}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return ListTile(title: Text(person.name));
      }
    }
    
    final faker = Faker();
    final dummyData = [
      ...List.generate(100, (index) => Person(name: faker.person.name())),
      Person(name: 'Øyvind')
    ];
    
    @freezed
    abstract class Person with _$Person {
      const factory Person({String name}) = _Person;
    }
    
    Login or Signup to reply.
  2. Solution with Sticky Side Header

    You have a package for that! The flutter_sticky_header package.

    enter image description here

    Interesting points about the solution:

    1. I used the diacritic package to remove the diacritics when indexing the list of Persons. You wouldn’t want to exclude all of Norway’s Øyvind, would you?
    2. I used a SplayTreeMap to keep the Map sorted.
    3. I used Flutter Hooks for the Persons mapping, happening only when contacts is changed.

    Full source code

    import 'dart:collection';
    
    import 'package:diacritic/diacritic.dart';
    import 'package:faker/faker.dart';
    import 'package:flutter/material.dart';
    import 'package:flutter_hooks/flutter_hooks.dart';
    import 'package:freezed_annotation/freezed_annotation.dart';
    import 'package:flutter_sticky_header/flutter_sticky_header.dart';
    
    part '66542479.alphabet.freezed.dart'; // File generated by freezed
    
    void main() {
      runApp(
        MaterialApp(
          debugShowCheckedModeBanner: false,
          title: 'Flutter Demo',
          home: DirectoryPage(contacts: dummyData),
        ),
      );
    }
    
    class DirectoryPage extends HookWidget {
      final List<Person> contacts;
    
      const DirectoryPage({Key key, this.contacts}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        final _mappedContacts = useState<SplayTreeMap<String, List<Person>>>(null);
        useEffect(() {
          _mappedContacts.value = contacts.fold<SplayTreeMap<String, List<Person>>>(
            SplayTreeMap<String, List<Person>>(),
            (acc, curr) {
              final firstChar = removeDiacritics(curr.name)[0];
              acc..[firstChar] ??= [];
              return acc..[firstChar].add(curr);
            },
          );
          return;
        }, [contacts]);
        return Scaffold(
          body: CustomScrollView(
            slivers: _mappedContacts.value.keys
                .map(
                  (firstChar) => _StickyHeaderList(
                      firstChar: firstChar,
                      persons: _mappedContacts.value[firstChar]),
                )
                .toList(),
          ),
        );
      }
    }
    
    class _StickyHeaderList extends StatelessWidget {
      final String firstChar;
      final List<Person> persons;
    
      const _StickyHeaderList({
        Key key,
        this.firstChar,
        this.persons,
      }) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return SliverStickyHeader(
          overlapsContent: true,
          header: _SideHeader(text: firstChar),
          sliver: SliverPadding(
            padding: const EdgeInsets.only(left: 60),
            sliver: SliverList(
              delegate: SliverChildListDelegate([
                ...persons.map((person) => PersonTile(person: person)).toList(),
                Divider(),
              ]),
            ),
          ),
        );
      }
    }
    
    class _SideHeader extends StatelessWidget {
      final String text;
      const _SideHeader({
        Key key,
        this.text,
      }) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Padding(
          padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0),
          child: Text(text, style: TextStyle(fontSize: 36.0)),
        );
      }
    }
    
    class PersonTile extends StatelessWidget {
      final Person person;
    
      const PersonTile({Key key, this.person}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return ListTile(title: Text(person.name));
      }
    }
    
    final faker = Faker();
    final dummyData = [
      ...List.generate(100, (index) => Person(name: faker.person.name())),
      Person(name: 'Øyvind')
    ];
    
    @freezed
    abstract class Person with _$Person {
      const factory Person({String name}) = _Person;
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search