Multi directional infinite list with Sticky headers for Flutter applications

Overview

Sticky Infinite List

pub package Awesome Flutter

Infinite list with sticky headers.

This package was made in order to make possible render infinite list in both directions with sticky headers, unlike most packages in Dart Pub.

Supports various header positioning. Also supports Vertical and Horizontal scroll list

It highly customizable and doesn't have any third party dependencies or native(Android/iOS) code.

In addition to default usage, this package exposes some classes, that can be overridden if needed. Also some classes it can be used inside Scrollable widgets independently from InfiniteList container.

This package uses CustomScrollView to perform scroll with all benefits for performance that Flutter provides.

Features

  • sticky headers within infinite list
  • multi directional infinite list
  • customization for sticky header position
  • horizontal sticky list support
  • dynamic header build on content scroll
  • dynamic min offset calculation on content scroll

Flutter before 1.20

If you're using Flutter version lower than 1.20 consider using v2.x.x.

Migration guide

If you using older MAJOR versions, please visit this migration guide

Property not defined

If you get similar error message during the build please read this section first.

Demo

Getting Started

Install package and import

import 'package:sticky_infinite_list/sticky_infinite_list.dart';

Package exposes InfiniteList, InfiniteListItem, StickyListItem, StickyListItemRenderObject classes

Examples

Simple example

To start using Infinite list with sticky headers, you need to create instance InfiniteList with builder specified.

No need to specify any additional config to make it work

import 'package:sticky_infinite_list/sticky_infinite_list.dart';

class Example extends StatelessWidget {
  
  @override
  Widget build(BuildContext context) {
    return InfiniteList(
      builder: (BuildContext context, int index) {
        /// Builder requires [InfiniteList] to be returned
        return InfiniteListItem(
          /// Header builder
          headerBuilder: (BuildContext context) {
            return Container(
              ///...
            );
          },
          /// Content builder
          contentBuilder: (BuildContext context) {
            return Container(
              ///...
            );
          },
        );
      }
    );
  }
}

Or with header overlay content

import 'package:sticky_infinite_list/sticky_infinite_list.dart';

class Example extends StatelessWidget {
  
  @override
  Widget build(BuildContext context) {
    return InfiniteList(
      builder: (BuildContext context, int index) {
        /// Builder requires [InfiniteList] to be returned
        return InfiniteListItem.overlay(
          /// Header builder
          headerBuilder: (BuildContext context) {
            return Container(
              ///...
            );
          },
          /// Content builder
          contentBuilder: (BuildContext context) {
            return Container(
              ///...
            );
          },
        );
      }
    );
  }
}

State

When min offset callback invoked or header builder is invoked object StickyState is passed as parameter

This object describes current state for sticky header.

class StickyState<I> {
  /// Position, that header already passed
  ///
  /// Value can be between 0.0 and 1.0
  ///
  /// If it's `0.0` - sticky in max start position
  ///
  /// `1.0` - max end position
  ///
  /// If [InfiniteListItem.initialHeaderBuild] is true, initial
  /// header render will be with position = 0
  final double position;

  /// Number of pixels, that outside of viewport
  ///
  /// If [InfiniteListItem.initialHeaderBuild] is true, initial
  /// header render will be with offset = 0
  /// 
  /// For header bottom positions (or right positions for horizontal)
  /// offset value also will be amount of pixels that was scrolled away
  final double offset;

  /// Item index
  final I index;

  /// If header is in sticky state
  ///
  /// If [InfiniteListItem.minOffsetProvider] is defined,
  /// it could be that header builder will be emitted with new state
  /// on scroll, but [sticky] will be false, if offset already passed
  /// min offset value
  ///
  /// WHen [InfiniteListItem.minOffsetProvider] is called, [sticky]
  /// will always be `false`. Since for min offset calculation
  /// offset itself not defined yet
  final bool sticky;

  /// Scroll item height.
  ///
  /// If [InfiniteListItem.initialHeaderBuild] is true, initial
  /// header render will be called without this value
  final double contentSize;
}

Extended configuration

Available configuration

Alongside with minimal config to start using.

InfiniteList allows you to define config for scroll list rendering

InfiniteList(
  /// Optional parameter to pass ScrollController instance
  controller: ScrollController(),
  
  /// Optional parameter
  /// to specify scroll direction
  /// 
  /// By default scroll will be rendered with just positive
  /// direction `InfiniteListDirection.forward`
  /// 
  /// If you need infinite list in both directions use `InfiniteListDirection.multi`
  direction: InfiniteListDirection.multi,
  
  /// Negative max child count.
  /// 
  /// Will be used only when `direction: InfiniteListDirection.multi`
  /// 
  /// If it's not provided, scroll will be infinite in negative direction
  negChildCount: 100,
  
  /// Positive max child count
  /// 
  /// Specifies number of elements for forward list
  /// 
  /// If it's not provided, scroll will be infinite in positive direction
  posChildCount: 100,
  
  /// ScrollView anchor value.
  anchor: 0.0,
  
  /// ScrollView physics value.
  physics: null,

  /// Item builder
  /// 
  /// Should return `InfiniteListItem`
  builder: (BuildContext context, int index) {
    return InfiniteListItem(
      //...
    )
  }
)

InfiniteListItem allows you to specify more options for you customization.

InfiniteListItem(
  /// See class description for more info
  /// 
  /// Forces initial header render when [headerStateBuilder]
  /// is specified.
  initialHeaderBuild: false,

  /// Simple Header builder
  /// that will be called once during List item render
  headerBuilder: (BuildContext context) {},
  
  /// Header builder, that will be invoked each time
  /// when header should change it's position
  /// 
  /// Unlike prev method, it also provides `state` of header
  /// position
  /// 
  /// This callback has higher priority than [headerBuilder],
  /// so if both header builders will be provided,
  /// [headerBuilder] will be ignored
  headerStateBuilder: (BuildContext context, StickyState<int> state) {},
  
  /// Content builder
  contentBuilder: (BuildContext context) {},
  
  /// Min offset invoker
  /// 
  /// This callback is called on each header position change,
  /// to define when header should be stick to the bottom of
  /// content.
  /// 
  /// If this method returns `0`,
  /// header will be in sticky state until list item
  /// will be visible inside view port
  /// 
  /// If this method not provided or it returns null, header
  /// will be sticky until offset equals to 
  /// header size
  minOffsetProvider: (StickyState<int> state) {},
  
  /// Header alignment against main axis direction
  ///
  /// See [HeaderMainAxisAlignment] for more info
  HeaderMainAxisAlignment mainAxisAlignment: HeaderMainAxisAlignment.start,

  /// Header alignment against cross axis direction
  ///
  /// See [HeaderCrossAxisAlignment] for more info
  HeaderCrossAxisAlignment crossAxisAlignment: HeaderCrossAxisAlignment.start,

  /// Header position against scroll axis for relative positioned headers
  ///
  /// Only for relative header positioning
  HeaderPositionAxis positionAxis: HeaderPositionAxis.mainAxis,

  /// List item padding, see [EdgeInsets] for more info
  EdgeInsets padding: const EdgeInsets.all(8.0),
  
  /// Scroll direction
  ///
  /// Can be vertical or horizontal (see [Axis] class)
  ///
  /// This value also affects how bottom or top
  /// edge header positioned headers behave
  scrollDirection: Axis.vertical,
);

Demos

Header alignment demo

Relative positioning

Relative cross axis positioning

Overlay positioning

Horizontal scroll demo

Reverse infinite scroll

Currently package doesn't support CustomScrollView.reverse option.

But same result can be achieved with defining anchor = 1 and posChildCount = 0. In that way viewport center will be stick to the bottom and positive list won't render anything.

Additionally you can specify header alignment to any side.

import 'package:sticky_infinite_list/sticky_infinite_list.dart';

class Example extends StatelessWidget {
  
  @override
  Widget build(BuildContext context) {
    return InfiniteList(
      anchor: 1.0,
      
      direction: InfiniteListDirection.multi,
      
      posChildCount: 0,
      
      builder: (BuildContext context, int index) {
        /// Builder requires [InfiniteList] to be returned
        return InfiniteListItem(
        
          mainAxisAlignment: HeaderMainAxisAlignment.end,
          crossAxisAlignment: HeaderCrossAxisAlignment.start,
          
          /// Header builder
          headerBuilder: (BuildContext context) {
            return Container(
              ///...
            );
          },
          /// Content builder
          contentBuilder: (BuildContext context) {
            return Container(
              ///...
            );
          },
        );
      }
    );
  }
}

Demo

For more info take a look at Example project

Available for override

In most cases it will be enough to just use InfiniteListItem

But in some cases you may need to add additional functionality to each item.

Luckily you can extend and override base InfiniteListItem class

/// Generic `I` is index type, by default list item uses `int`

class SomeCustomListItem<I> extends InfiniteListItem<I> {
  /// Header alignment against main axis direction
  ///
  /// See [HeaderMainAxisAlignment] for more info
  @override
  final HeaderMainAxisAlignment mainAxisAlignment = HeaderMainAxisAlignment.start;

  /// Header alignment against cross axis direction
  ///
  /// See [HeaderCrossAxisAlignment] for more info
  @override
  final HeaderCrossAxisAlignment crossAxisAlignment = HeaderCrossAxisAlignment.start;

  /// Header position against scroll axis for relative positioned headers
  ///
  /// See [HeaderPositionAxis] for more info
  @override
  final HeaderPositionAxis positionAxis = HeaderPositionAxis.mainAxis;

  /// If header should overlay content or not
  @override
  final bool overlayContent = false;

  /// Let item builder know if it should watch
  /// header position changes
  /// 
  /// If this value is `true` - it will invoke [buildHeader]
  /// each time header position changes
  @override
  bool get watchStickyState => true;

  /// Let item builder know that this class
  /// provides header
  /// 
  /// If it returns `false` - [buildHeader] will be ignored 
  /// and never called
  @override
  bool get hasStickyHeader => true;

  /// This methods builds header
  /// 
  /// If [watchStickyState] is `true`,
  /// it will be invoked on each header position change
  /// and `state` option will be provided
  /// 
  /// Otherwise it will be called only once on initial render
  /// and each header position change won't invoke this method.
  /// 
  /// Also in that case `state` will be `null`
  @override
  Widget buildHeader(BuildContext context, [StickyState<I> state]) {}

  /// Content item builder
  /// 
  /// This method invoked only once
  @override
  Widget buildContent(BuildContext context) {}

  /// Called during init state (see [Statefull] widget [State.initState])
  /// 
  /// For additional information about [Statefull] widget `initState`
  /// lifecycle - see Flutter docs
  @protected
  @mustCallSuper
  void initState() {}

  /// Called during item dispose (see [Statefull] widget [State.dispose])
  /// 
  /// For additional information about [Statefull] widget `dispose`
  /// lifecycle - see Flutter docs
  @protected
  @mustCallSuper
  void dispose() {}
}

Need more override?..

Alongside with list item override, to use inside InfiniteList builder, you can also use or extend StickyListItem, that exposed by this package too, independently.

This class uses Stream to inform it's parent about header position changes

Also it requires to be rendered inside Scrollable widget and Viewport, since it subscribes to scroll event and calculates position against Viewport coordinates (see StickyListItemRenderObject class for more information)

For example

Widget build(BuildContext context) {
  return SingleChildScrollView(
    child: Column(
      children: <Widget>[
        Container(
          height: height,
          color: Colors.lightBlueAccent,
          child: Placeholder(),
        ),
        StickyListItem<String>(
          streamSink: _headerStream.sink, /// stream to update header during scroll
          header: Container(
            height: _headerHeight,
            width: double.infinity,
            color: Colors.orange,
            child: Center(
              child: StreamBuilder<StickyState<String>>(
                stream: _headerStream.stream, /// stream to update header during scroll
                builder: (_, snapshot) {
                  if (!snapshot.hasData) {
                    return Container();
                  }

                  final position = (snapshot.data.position * 100).round();

                  return Text('Positioned relative. Position: $position%');
                },
              ),
            ),
          ),
          content: Container(
            height: height,
            color: Colors.blueAccent,
            child: Placeholder(),
          ),
          itemIndex: "single-child",
        ),
        StickyListItem<String>.overlay(
          streamSink: _headerOverlayStream.sink, /// stream to update header during scroll
          header: Container(
            height: _headerHeight,
            width: double.infinity,
            color: Colors.orange,
            child: Center(
              child: StreamBuilder<StickyState<String>>(
                stream: _headerOverlayStream.stream, /// stream to update header during scroll
                builder: (_, snapshot) {
                  if (!snapshot.hasData) {
                    return Container();
                  }

                  final position = (snapshot.data.position * 100).round();

                  return Text('Positioned overlay. Position: $position%');
                },
              ),
            ),
          ),
          content: Container(
            height: height,
            color: Colors.lightBlueAccent,
            child: Placeholder(),
          ),
          itemIndex: "single-overlayed-child",
        ),
        Container(
          height: height,
          color: Colors.cyan,
          child: Placeholder(),
        ),
      ],
    ),
  );
}

This code will render single child scroll with 4 widgets. Two middle items - items with sticky header.

Demo

For more complex example please take a look at "Single Example" page in Example project

Changelog

Please see the Changelog page to know what's recently changed.

Bugs/Requests

If you encounter any problems feel free to open an issue. If you feel the library is missing a feature, please raise a ticket on Github and I'll look into it. Pull request are also welcome.

Known issues

Currently this package can't work with reverse scroll. For some reason flutter calculates coordinate for negative list items in a different way in reverse mode, comparing to regular scroll direction.

But there is an workaround can be used, described in Reverse infinite scroll

Flutter version related errors

Named parameter clipBehavior isn't defined error

If you get this kind of error, most likely you are using Flutter 1.17. If so, please ensure that you're using 2.x.x version.

Comments
  • Simple example for quick start

    Simple example for quick start

    Hi, thank you for this plugin, it looks nice even though and struggling to quickly integrate it in my code.

    Would it be possible to please give a real list example as could be used in an app?

    Forexample below is my code using another sticky header plugin, how can I quickly implement your plugin in this flow.

    Widget buildListMessage() {
        print('conversationId in the list wig is: $hasConversationId');
        print('private chat it: $privateChatId');
        return Flexible(
          child: privateChatId == ''
              ? Center(
                  child: CircularProgressIndicator(
                      valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF180018))))
              : StreamBuilder(
                  ///STREAM CONVARSATION WITH THIS USER
                  stream: _messages,
                  builder: (context, snapshot) {
                    if (!snapshot.hasData) {
                      //If there is no conversation of this is set hasConversation
                      hasConversationId = false;
                      return Center(
                          child: CircularProgressIndicator(
                              valueColor: AlwaysStoppedAnimation<Color>(
                                  Color(0xFF180018))));
                    } else {
                      listMessage = snapshot.data.documents;
                      if (snapshot.data.documents.length > 0)
                        //If there is no conversation of this is set hasConversation
                        hasConversationId = true;
                      print(
                          'conversationId inside list return: $hasConversationId');
                      var newMap = groupBy(
                          listMessage,
                          (obj) => DateFormat('y-MM-dd').format(
                              DateTime.fromMillisecondsSinceEpoch(
                                  int.parse(obj['timestamp']))));
    							  
    							  
                      return CustomScrollView(
                          reverse: true,
                        slivers: newMap.entries
                            .map<Widget>((item) => buildDateItem(context, item))
                            .toList(),
                      );
                    }
                  },
                ),
        );
      }
    
      Widget buildDateItem(BuildContext context, item) {
        print('Map in listview: ${item.value}');
        return SliverStickyHeader(
          header: Center(
            child: Container(
              padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 5.0),
              margin: EdgeInsets.only(top: 5.0, bottom: 5.0),
              decoration: BoxDecoration(
                  color: Colors.black54,
                  borderRadius: BorderRadius.all(Radius.circular(30))),
              child: Text(
                dateFx(item.key),
                style: const TextStyle(
                  color: Colors.white,
                ),
                textAlign: TextAlign.center,
              ),
            ),
          ),
          sliver: SliverList(
            delegate: SliverChildBuilderDelegate(
              (BuildContext context, int index) => buildItem(context, index, item.value[index]),
              childCount: item.value.length,
            ),
          ),
        );
      }
    

    Thanks.

    opened by pkitatta 24
  • Optional headers

    Optional headers

    Hi! I want to group several items in a same header. Is it possible? Example:

    Header content content --> no header Header content Header content content --> no header content --> no header

    Thanks!

    opened by unveloper 18
  • Erro => scrollDirection: Axis.horizontal

    Erro => scrollDirection: Axis.horizontal

    The following NoSuchMethodError was thrown building _StickySliverListItem(dependencies: I/flutter (31517): [MediaQuery], state: _StickySliverListItemState#030fb): I/flutter (31517): The method 'addListener' was called on null. I/flutter (31517): Receiver: null I/flutter (31517): Tried calling: addListener(Closure: () => void from Function 'markNeedsPaint':.)

    bug 
    opened by MarceloRab 16
  • Sort items descending and scroll to the end of the list by default

    Sort items descending and scroll to the end of the list by default

    Describe the behavior you'd like to achieve Hi. I have a comment section in my app where I am using this package for. I am fetching data from Firebase and then display the data in ascending order from top to bottom (older to newer). That works fine, but I want to have the list scrolled to the bottom (most recent comment) by default, when I navigate to the screen with this widget. How can I do that?

    Thanks for the help.

    Code sample that you've tried so far (if you have any) InfiniteList( controller: ScrollController(), direction: InfiniteListDirection.multi, negChildCount: 0, posChildCount: postCommentDocs.length, anchor: 0, builder: (BuildContext context, int index) { return InfiniteListItem( headerBuilder: (BuildContext context) { // return My headers }, contentBuilder: (BuildContext context) { // return My content }, minOffsetProvider: (StickyState<int> state) => 50, mainAxisAlignment: HeaderMainAxisAlignment.start, crossAxisAlignment: HeaderCrossAxisAlignment.start, positionAxis: HeaderPositionAxis.crossAxis, ); }, ),

    question 
    opened by AkbarBakhshi 11
  • Can't slide when the list is not high enough

    Can't slide when the list is not high enough

    I add BouncingScrollPhysics() ,but Can't slide when the list is not high enough. Example: InfiniteList( /// Scroll controller. Optional controller: ScrollController(),

                  /// Render scroll in both directions. `Optional`
                  direction: InfiniteListDirection.multi,
    
                  /// Render 100 elements in negative direction. `Optional`
                  ///
                  /// If it's not provided, scroll will be infinite in negative direction
                  ///
                  /// Will be ignored if [direction] is forward
                  ///
                  /// If it's `null`, list will be infinite
                  minChildCount: -0,
    
                  /// Render 100 elements in positive direction. `Optional`
                  ///
                  /// If it's not provided, scroll will be infinite in positive direction
                  ///
                  /// If it's `null`, list will be infinite
                  maxChildCount: 1,
                  physics: BouncingScrollPhysics(),
                  /// ViewPort anchor value. See [ScrollView] docs for more info
                  anchor: 0.0,
    
                  /// Item builder
                  builder: (BuildContext context, int index) {
                    /// Builder requires [InfiniteList] to be returned
                    return InfiniteListItem(
                      /// If header should be build during initial render.
                      ///
                      /// Will be ignored with just [headerBuilder] builder specified
                      initialHeaderBuild: true,
    
                      /// Header builder with state
                      ///
                      /// Will be invoked each time header changes it's position
                      ///
                      /// If you don't need to rerender header on each
                      /// position change - use [headerBuilder] builder
                      headerStateBuilder: (BuildContext context, StickyState<int> state) {
                        return Container(
                          alignment: Alignment.center,
                          width: double.infinity,
                          height: 50,
                          child: Text("Header $index"),
                          color: Colors.orange.withOpacity(state.position),
                        );
                      },
    
                      /// This is just example
                      ///
                      /// In you application you should use or
                      /// [headerBuilder] or [headerStateBuilder],
                      /// but not both
                      ///
                      /// If both is specified, this invoker will be ignored
                      headerBuilder: (BuildContext context) {
                        return Container(
                          alignment: Alignment.center,
                          width: double.infinity,
                          height: 50,
                          child: Text("Header $index"),
                          color: Colors.orange,
                        );
                      },
    
                      /// Content builder
                      contentBuilder: (BuildContext context) {
                        return Container(
                          width: double.infinity,
                          height: 300,
                          child: Text(
                            "Content $index",
                            style: TextStyle(
                              color: Colors.white,
                            ),
                          ),
                          color: Colors.blueAccent,
                        );
                      },
    
                      /// Min offset after which it
                      /// should stick to the bottom edge
                      /// of container
                      minOffsetProvider: (StickyState<int> state) => 50,
    
                      /// Header alignment
                      ///
                      /// Currently it supports top left,
                      /// top right, bottom left and bottom right alignments
                      ///
                      /// By default [HeaderAlignment.topLeft]
                      headerAlignment: HeaderAlignment.topLeft,
    
    
                    );
                  }
              )
    
    opened by w497763094 11
  • Rebuilding all InfiniteListItem on setState

    Rebuilding all InfiniteListItem on setState

    Describe the bug This can be bug with flutter framework, but here is what is going on. When Infinitive list is set to infinitive bidirectional and scrolling to "positive" index side - after setState - all InfiniteListItems are built. With same parameters and scrolling to "negative" index side - after setState - only visible InfiniteListItems are build.

    To Reproduce Steps to reproduce the behavior:

    1. Scroll down (positive index side)
    2. After scroll ended - setState is called
    3. InfiniteListItems are built starting with index -1 (check log - debugPrint added)
    4. Scroll up (negative index side)
    5. After scroll ended - setState is called
    6. Only visible InfiniteListItems are built (check log - debugPrint added)

    Expected behavior setState should rebuild only "visible" InfiniteListItems (inside viewport) on both side the same way.

    Code example

    import 'package:sticky_infinite_list/sticky_infinite_list.dart';
    import 'package:flutter/material.dart';
    
    class StickyInfiniteList extends StatefulWidget {
      @override
      StickyInfiniteListState createState() => StickyInfiniteListState();
    }
    
    class StickyInfiniteListState extends State<StickyInfiniteList> {
    
      @override
      Widget build(BuildContext context) {
        return  NotificationListener<ScrollNotification>(
            onNotification: (ScrollNotification scrollInfo) {
              if (scrollInfo is ScrollEndNotification) {
                setState(() {
                  debugPrint('setState');
                });
              }
              return true;
            },
          child: InfiniteList(
              controller: ScrollController(),
              direction: InfiniteListDirection.multi,
              builder: (BuildContext context, int index) {
                return InfiniteListItem(
                  contentBuilder: (BuildContext context) {
                    debugPrint('builder $index');
                    return Container(
                      height: 300,
                      child: Text(
                        "Content $index",
                        style: TextStyle(
                          color: Colors.white,
                        ),
                      ),
                      color: Colors.blueAccent,
                    );
                  },
                );
              }
          ),
        );
      }
    }
    

    Environment (please complete the following information): Doctor summary (to see all details, run flutter doctor -v): [√] Flutter (Channel stable, 1.22.3, on Microsoft Windows [Version 10.0.18363.1139], locale hr-HR) [√] Android toolchain - develop for Android devices (Android SDK version 30.0.2) [!] Android Studio (version 4.1.0) X Flutter plugin not installed; this adds Flutter specific functionality. X Dart plugin not installed; this adds Dart specific functionality. [√] Connected device (1 available)

    bug 
    opened by sh00le 10
  • Grouping?

    Grouping?

    I might be missing something obvious, but if the builder has both header and content, how do I make grouping? E.g. the header would be "A" and the content would be "Abby", "Adam", "Ahmad".

    question 
    opened by wiradikusuma 10
  • how to use it with NestedScrollView

    how to use it with NestedScrollView

    import 'package:intl/intl.dart';
    import 'package:flutter/material.dart';
    import 'package:sticky_infinite_list/sticky_infinite_list.dart';
    
    void main() => runApp(MyApp());
    
    class MyApp extends StatelessWidget {
      // This widget is the root of your application.
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            // This is the theme of your application.
            //
            // Try running your application with "flutter run". You'll see the
            // application has a blue toolbar. Then, without quitting the app, try
            // changing the primarySwatch below to Colors.green and then invoke
            // "hot reload" (press "r" in the console where you ran "flutter run",
            // or simply save your changes to "hot reload" in a Flutter IDE).
            // Notice that the counter didn't reset back to zero; the application
            // is not restarted.
            primarySwatch: Colors.blue,
          ),
          home: MyHomePage(title: 'Flutter Demo Home Page'),
          routes: {
            SingleChildScrollPage.ROUTE: (_) => SingleChildScrollPage(),
          },
        );
      }
    }
    
    class MyHomePage extends StatefulWidget {
      MyHomePage({Key key, this.title}) : super(key: key);
    
      // This widget is the home page of your application. It is stateful, meaning
      // that it has a State object (defined below) that contains fields that affect
      // how it looks.
    
      // This class is the configuration for the state. It holds the values (in this
      // case the title) provided by the parent (in this case the App widget) and
      // used by the build method of the State. Fields in a Widget subclass are
      // always marked "final".
    
      final String title;
    
      @override
      _MyHomePageState createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage> {
      final StreamController<Settings> _streamController =
          StreamController<Settings>.broadcast();
      final ScrollController _scrollController = ScrollController();
      Settings _settings = Settings();
    
      @override
      Widget build(BuildContext context) => Scaffold(
            appBar: AppBar(
              title: Text(widget.title),
            ),
            body: NestedScrollView(
              headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
                return <Widget>[
                  SliverAppBar(
                    expandedHeight: 230.0,
                    pinned: true,
                    flexibleSpace: FlexibleSpaceBar(
                      title: Text('复仇者联盟'),
                      background: Image.network(
                        'http://img.haote.com/upload/20180918/2018091815372344164.jpg',
                        fit: BoxFit.fitHeight,
                      ),
                    ),
                  )
                ];
              },
              body: ScrollWidget(
                settings: _settings,
                scrollController: _scrollController,
                stream: _streamController.stream,
              ),
            ),
          );
    
      @override
      void dispose() {
        super.dispose();
    
        _streamController.close();
      }
    }
    
    class ScrollWidget extends StatelessWidget {
      final Stream<Settings> stream;
      final ScrollController scrollController;
      final Settings settings;
    
      const ScrollWidget({
        Key key,
        this.stream,
        this.scrollController,
        this.settings,
      }) : super(key: key);
    
      @override
      Widget build(BuildContext context) => InfiniteList(
            /// when direction changes dynamically, Flutter
            /// won't rerender scroll completely,
            /// which means that gesture detector on scroll itself
            /// remains from original direction
            key: Key(settings.scrollDirection.toString()),
            scrollDirection: settings.scrollDirection,
            anchor: settings.anchor,
            controller: scrollController,
            direction: settings.multiDirection
                ? InfiniteListDirection.multi
                : InfiniteListDirection.single,
            negChildCount: settings.negCount,
            posChildCount: settings.posCount,
            physics: settings.physics,
            builder: (context, index) {
              final date = DateTime.now().add(Duration(
                days: index,
              ));
    
              if (settings.overlay) {
                return InfiniteListItem.overlay(
                  mainAxisAlignment: settings.mainAxisAlignment,
                  crossAxisAlignment: settings.crossAxisAlignment,
                  headerStateBuilder: (context, state) => Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: Container(
                      decoration: BoxDecoration(
                        shape: BoxShape.circle,
                        color: Colors.orange.withOpacity(1 - state.position),
                      ),
                      height: 70,
                      width: 70,
                      child: Column(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: <Widget>[
                          Text(
                            date.day.toString(),
                            style: TextStyle(
                              fontSize: 21,
                              color: Colors.black87,
                              fontWeight: FontWeight.w600,
                            ),
                          ),
                          Text(
                            DateFormat.MMM().format(date),
                            style: TextStyle(
                              height: .7,
                              fontSize: 17,
                              color: Colors.black87,
                              fontWeight: FontWeight.w400,
                            ),
                          )
                        ],
                      ),
                    ),
                  ),
                  contentBuilder: (context) => Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: Container(
                      decoration: BoxDecoration(
                        borderRadius: BorderRadius.circular(10),
                        color: Colors.blueAccent,
                      ),
                      height: settings.contentHeight,
                      width: settings.contentWidth,
                      child: Center(
                        child: Text(
                          DateFormat.yMMMMd().format(date),
                          style: TextStyle(fontSize: 18, color: Colors.white),
                        ),
                      ),
                    ),
                  ),
                );
              }
    
              return InfiniteListItem(
                mainAxisAlignment: settings.mainAxisAlignment,
                crossAxisAlignment: settings.crossAxisAlignment,
                positionAxis: settings.positionAxis,
                headerStateBuilder: (context, state) => Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: Container(
                    decoration: BoxDecoration(
                      shape: BoxShape.circle,
                      color: Colors.orange.withOpacity(1 - state.position),
                    ),
                    height: 70,
                    width: 70,
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: <Widget>[
                        Text(
                          date.day.toString(),
                          style: TextStyle(
                            fontSize: 21,
                            color: Colors.black87,
                            fontWeight: FontWeight.w600,
                          ),
                        ),
                        Text(
                          DateFormat.MMM().format(date),
                          style: TextStyle(
                            height: .7,
                            fontSize: 17,
                            color: Colors.black87,
                            fontWeight: FontWeight.w400,
                          ),
                        )
                      ],
                    ),
                  ),
                ),
                contentBuilder: (context) => Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: Container(
                    decoration: BoxDecoration(
                      borderRadius: BorderRadius.circular(10),
                      color: Colors.blueAccent,
                    ),
                    height: settings.contentHeight,
                    width: settings.contentWidth,
                    child: Center(
                      child: Text(
                        DateFormat.yMMMMd().format(date),
                        style: TextStyle(fontSize: 18, color: Colors.white),
                      ),
                    ),
                  ),
                ),
              );
            },
          );
    }
    
    enum ScrollPhysicsEnum {
      PLATFORM,
      IOS,
      ANDROID,
    }
    
    class Settings {
      int negCount;
      int posCount;
      bool multiDirection;
      HeaderMainAxisAlignment mainAxisAlignment;
      HeaderCrossAxisAlignment crossAxisAlignment;
      HeaderPositionAxis positionAxis;
      double anchor;
      Axis scrollDirection;
      ScrollPhysicsEnum physicsType;
      bool overlay;
    
      bool get scrollVertical => scrollDirection == Axis.vertical;
    
      ScrollPhysics get physics {
        switch (physicsType) {
          case ScrollPhysicsEnum.PLATFORM:
            return null;
    
          case ScrollPhysicsEnum.ANDROID:
            return ClampingScrollPhysics();
    
          case ScrollPhysicsEnum.IOS:
            return BouncingScrollPhysics();
        }
    
        return null;
      }
    
      double get contentHeight {
        if (scrollVertical) {
          return 300;
        }
    
        return double.infinity;
      }
    
      double get contentWidth {
        if (scrollVertical) {
          return double.infinity;
        }
    
        return 300;
      }
    
      Settings({
        this.negCount,
        this.posCount,
        this.mainAxisAlignment = HeaderMainAxisAlignment.start,
        this.crossAxisAlignment = HeaderCrossAxisAlignment.start,
        this.positionAxis = HeaderPositionAxis.mainAxis,
        this.multiDirection = false,
        this.anchor = 0,
        this.scrollDirection = Axis.vertical,
        this.physicsType = ScrollPhysicsEnum.PLATFORM,
        this.overlay = false,
      });
    }
    
    class SingleChildScrollPage extends StatefulWidget {
      static const String ROUTE = "/single-child";
    
      @override
      _SingleChildScrollPageState createState() => _SingleChildScrollPageState();
    }
    
    class _SingleChildScrollPageState extends State<SingleChildScrollPage> {
      static const double _headerHeight = 50;
    
      final StreamController<StickyState<String>> _headerStream =
          StreamController<StickyState<String>>.broadcast();
      final StreamController<StickyState<String>> _headerOverlayStream =
          StreamController<StickyState<String>>.broadcast();
    
      @override
      Widget build(BuildContext context) {
        final height = MediaQuery.of(context).size.height;
    
        return Scaffold(
          appBar: AppBar(
            title: const Text('Single element example'),
          ),
          body: SingleChildScrollView(
            child: Column(
              children: <Widget>[
                Container(
                  height: height,
                  color: Colors.lightBlueAccent,
                  child: Placeholder(),
                ),
                StickyListItem<String>(
                  streamSink: _headerStream.sink,
                  header: Container(
                    height: _headerHeight,
                    width: double.infinity,
                    color: Colors.orange,
                    child: Center(
                      child: StreamBuilder<StickyState<String>>(
                        stream: _headerStream.stream,
                        builder: (_, snapshot) {
                          if (!snapshot.hasData) {
                            return Container();
                          }
    
                          final position = (snapshot.data.position * 100).round();
    
                          return Text('Positioned relative. Position: $position%');
                        },
                      ),
                    ),
                  ),
                  content: Container(
                    height: height,
                    color: Colors.blueAccent,
                    child: Placeholder(),
                  ),
                  itemIndex: "single-child",
                ),
                StickyListItem<String>.overlay(
                  streamSink: _headerOverlayStream.sink,
                  header: Container(
                    height: _headerHeight,
                    width: double.infinity,
                    color: Colors.orange,
                    child: Center(
                      child: StreamBuilder<StickyState<String>>(
                        stream: _headerOverlayStream.stream,
                        builder: (_, snapshot) {
                          if (!snapshot.hasData) {
                            return Container();
                          }
    
                          final position = (snapshot.data.position * 100).round();
    
                          return Text('Positioned overlay. Position: $position%');
                        },
                      ),
                    ),
                  ),
                  content: Container(
                    height: height,
                    color: Colors.lightBlueAccent,
                    child: Placeholder(),
                  ),
                  itemIndex: "single-overlayed-child",
                ),
                Container(
                  height: height,
                  color: Colors.cyan,
                  child: Placeholder(),
                ),
              ],
            ),
          ),
        );
      }
    
      @override
      void dispose() {
        super.dispose();
    
        _headerStream.close();
        _headerOverlayStream.close();
      }
    }
    
    question 
    opened by fangshengfy 10
  • Expand / collapse content

    Expand / collapse content

    Describe the behavior you'd like to achieve Hello, I want to expand / collapse items attached to a header when I click on the header. I have trouble to achieve this result as the builder of InfiniteList require an InfiniteListItem and not a widget. Thank you for your help and for this amazing lib

    question 
    opened by thegrxp 9
  • Not working with Flutter 1.20 and higher

    Not working with Flutter 1.20 and higher

    Not working with Flutter 1.17.5 (which is currently on dev channel)

    /C:/dev/flutter/.pub-cache/hosted/pub.dartlang.org/sticky_infinite_list-2.1.0+2/lib/render.dart:50:11: Error: No named parameter with the name 'overflow'.
              overflow: overflow,
              ^^^^^^^^
    

    Looks like overflow param was removed from RenderStack

    bug 
    opened by tudor07 8
  • What is the best approach to stick one widget on scroll

    What is the best approach to stick one widget on scroll

    I want to stick the pink area. Should I use CustomScrollView instead of Scaffold? What are the features that CustomScrollView provides but Scaffold doesn't?

    image

    opened by ycgambo 8
  • Divider

    Divider

    It would be great if dividers can be added without much hassle. I've made a fork, and have divider working, but I'm lazy so it only works for my use case (forward-only, vertical-only list).

    https://github.com/wiradikusuma/flutter_sticky_infinite_list/commit/7d37eaa0209eed1f7c0b9dd3812c61fdc3904647

    Hopefully I can implement a proper solution and submit a pull request. Or maybe some other brave soul would do it first #hint

    enhancement 
    opened by wiradikusuma 0
  • Added optional before and after slivers to sandwich the list.

    Added optional before and after slivers to sandwich the list.

    Sometimes you have your own slivers in addition to the list, e.g. a SliverPersistentHeader on top and SliverToBoxAdapter at the bottom. These 2 properties allow you to do that.

    Dart version bumped to 2.2.2 to make use of the spread operator.

    opened by wiradikusuma 8
Releases(4.0.1)
Owner
Denis Beketsky
Denis Beketsky
A collection of Screens and attractive UIs built with Flutter ready to be used in your applications. No external libraries are used. Just download, add to your project and use.

Flutter Screens A collection of Login Screens, Buttons, Loaders and Widgets with attractive UIs, built with Flutter, ready to be used in your applicat

Samarth Agarwal 5k Dec 31, 2022
A small splashscreen used as intro for flutter applications easily with a lot of customizations ❤️🥳

Splash Screen A splashscreen package to be used for an intro for any flutter application easily with a lot of customization Currently Supported by awe

DPLYR 283 Dec 30, 2022
An advance flutter UI Kit for developing responsive, cross platform applications.

Liquid Build fast, responsive, cross platform apps with Liquid. Liquid is an open source UI toolkit for developing cross platform apps in Flutter. Qui

StackOrient Pvt. Ltd. 29 Dec 9, 2022
On-boarding page for flutter applications

onboarding This is a sample flutter onboarding plugin you use to attract first-time users when they land on your page, hence the name onboarding. You

Eyoel Defare 9 Nov 2, 2022
🐱‍👤 Flutter-Animation 🔥 🔥 List Animated Staggered Animations

??‍?? Staggered Animations made with algeria ❤

Hmida 17 Nov 22, 2022
An awesome list that curates the best Flutter libraries, tools, tutorials, articles and more.

Flutter is Google’s UI toolkit for building beautiful, natively compiled applications for mobile, web, and desktop from a single codebase. If you appr

Robert Felker 44.6k Dec 30, 2022
filterList is a flutter package which provide utility to search/filter data from provided dynamic list.

filter_list Plugin FilterList is a flutter package which provide utility to search/filter on the basis of single/multiple selection from provided dyna

Sonu Sharma 156 Dec 24, 2022
A contact list UI clone for trainees during a national mobile development training session

contactapp A contact list UI clone for trainees during a national mobile development training session This project was built during a training session

bayorwor 4 Dec 14, 2021
Generate a timeline for a list

Timeline A flutter package that allows you to create basic timelines on your flutter application. This is customizable and easy to plugin to your appl

Rejish Radhakrishnan 65 Nov 10, 2022
🔔 A flutter package to create cool and beautiful text animations. [Flutter Favorite Package]

Animated Text Kit A flutter package which contains a collection of some cool and awesome text animations. Recommended package for text animations in C

Ayush Agarwal 1.4k Jan 6, 2023
This repository demonstrates use of various widgets in flutter and tricks to create beautiful UI elements in flutter for Android and IOS

AwesomeFlutterUI The purpose of this repository is to demonstrate the use of different widgets and tricks in flutter and how to use them in your proje

Subir Chakraborty 132 Nov 13, 2022
This is a Flutter URL preview plugin for Flutter that previews the content of a URL

flutter_link_preview This is a URL preview plugin that previews the content of a URL Language: English | 中文简体 Special feature Use multi-processing to

yung 67 Nov 2, 2022
Flutter liquid swipe - Liquid Swipe Animation Built With Flutter

Flutter Liquid Swipe liquid Swipe animation is amazing and its Created for iOS P

Jahongir Anvarov 6 Dec 1, 2022
A candy sorter game made with Flutter for the march flutter challenge.

A candy sorter game made with Flutter for the march flutter challenge.

Debjeet Das 1 Apr 9, 2022
✨ A collection of loading indicators animated with flutter. Heavily Inspired by http://tobiasahlin.com/spinkit.

✨ Flutter Spinkit A collection of loading indicators animated with flutter. Heavily inspired by @tobiasahlin's SpinKit. ?? Installing dependencies:

Jeremiah Ogbomo 2.7k Dec 30, 2022
A Flutter library for gradually painting SVG path objects on canvas (drawing line animation).

drawing_animation From static SVG assets See more examples in the showcasing app. Dynamically created from Path objects which are animated over time m

null 442 Dec 27, 2022
Flutter package for creating awesome animations.

?? Simple Animations Simple Animations is a powerful package to create beautiful custom animations in no time. ?? fully tested ?? well documented ?? e

Felix Blaschke 879 Dec 31, 2022
Fun canvas animations in Flutter based on time and math functions.

funvas Flutter package that allows creating canvas animations based on time and math (mostly trigonometric) functions. The name "funvas" is based on F

null 472 Jan 9, 2023
A flutter package that adds support for vector data based animations.

animated_vector Description and inspiration A package that adds support for vector data based animations. The data format is heavily inspired from the

Potato Open Sauce Project 6 Apr 26, 2022