Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save g33kidd/c6781191eba85a6d98f610e0cbc82cb1 to your computer and use it in GitHub Desktop.
Save g33kidd/c6781191eba85a6d98f610e0cbc82cb1 to your computer and use it in GitHub Desktop.
Firestore Animated List implementation.
// Copyright 2017, the Flutter project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:collection';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
typedef Widget FirestoreAnimatedListItemBuilder(BuildContext context,
DocumentSnapshot snapshot,
Animation<double> animation,
int index,);
/// An AnimatedList widget that is bound to a query
class FirestoreAnimatedList extends StatefulWidget {
/// Creates a scrolling container that animates items when they are inserted or removed.
FirestoreAnimatedList({
Key key,
@required this.query,
@required this.itemBuilder,
this.defaultChild,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.controller,
this.primary,
this.physics,
this.shrinkWrap = false,
this.padding,
this.duration = const Duration(milliseconds: 300),
}) : super(key: key) {
assert(itemBuilder != null);
}
/// A Firebase query to use to populate the animated list
final Query query;
/// A widget to display while the query is loading. Defaults to an empty
/// Container().
final Widget defaultChild;
/// Called, as needed, to build list item widgets.
///
/// List items are only built when they're scrolled into view.
///
/// The [DataSnapshot] parameter indicates the snapshot that should be used
/// to build the item.
///
/// Implementations of this callback should assume that [AnimatedList.removeItem]
/// removes an item immediately.
final FirestoreAnimatedListItemBuilder itemBuilder;
/// The axis along which the scroll view scrolls.
///
/// Defaults to [Axis.vertical].
final Axis scrollDirection;
/// Whether the scroll view scrolls in the reading direction.
///
/// For example, if the reading direction is left-to-right and
/// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from
/// left to right when [reverse] is false and from right to left when
/// [reverse] is true.
///
/// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view
/// scrolls from top to bottom when [reverse] is false and from bottom to top
/// when [reverse] is true.
///
/// Defaults to false.
final bool reverse;
/// An object that can be used to control the position to which this scroll
/// view is scrolled.
///
/// Must be null if [primary] is true.
final ScrollController controller;
/// Whether this is the primary scroll view associated with the parent
/// [PrimaryScrollController].
///
/// On iOS, this identifies the scroll view that will scroll to top in
/// response to a tap in the status bar.
///
/// Defaults to true when [scrollDirection] is [Axis.vertical] and
/// [controller] is null.
final bool primary;
/// How the scroll view should respond to user input.
///
/// For example, determines how the scroll view continues to animate after the
/// user stops dragging the scroll view.
///
/// Defaults to matching platform conventions.
final ScrollPhysics physics;
/// Whether the extent of the scroll view in the [scrollDirection] should be
/// determined by the contents being viewed.
///
/// If the scroll view does not shrink wrap, then the scroll view will expand
/// to the maximum allowed size in the [scrollDirection]. If the scroll view
/// has unbounded constraints in the [scrollDirection], then [shrinkWrap] must
/// be true.
///
/// Shrink wrapping the content of the scroll view is significantly more
/// expensive than expanding to the maximum allowed size because the content
/// can expand and contract during scrolling, which means the size of the
/// scroll view needs to be recomputed whenever the scroll position changes.
///
/// Defaults to false.
final bool shrinkWrap;
/// The amount of space by which to inset the children.
final EdgeInsets padding;
/// The duration of the insert and remove animation.
///
/// Defaults to const Duration(milliseconds: 300).
final Duration duration;
@override
FirebaseAnimatedListState createState() => new FirebaseAnimatedListState();
}
class FirebaseAnimatedListState extends State<FirestoreAnimatedList> {
final _animatedListKey = GlobalKey<AnimatedListState>();
FirestoreList _list;
@override
void initState() {
super.initState();
_subscribe();
}
@override
void didUpdateWidget(FirestoreAnimatedList oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.query != oldWidget.query) {
_subscribe();
}
}
void _subscribe() {
_list?.dispose();
_list = FirestoreList(
query: widget.query,
onInitialData: _onInitialData,
onChildAdded: _onChildAdded,
onChildChanged: _onChildChanged,
onChildRemoved: _onChildRemoved,
onError: _onError,
);
_list.listen();
}
void _onInitialData() {
setState(() {});
}
void _onChildAdded(int index, DocumentSnapshot snapshot) {
_animatedListKey.currentState.insertItem(index, duration: widget.duration);
}
void _onChildChanged(int index, DocumentSnapshot snapshot) {
setState(() {});
}
void _onChildRemoved(int index, DocumentSnapshot snapshot) {
_animatedListKey.currentState.removeItem(
index,
(BuildContext context, Animation<double> animation) {
return widget.itemBuilder(context, snapshot, animation, index);
},
duration: widget.duration,
);
}
void _onError(dynamic error, StackTrace trace) {
print('$error\n$trace');
}
@override
void dispose() {
_list?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (!_list.isLoaded) {
return widget.defaultChild ?? new Container();
}
return AnimatedList(
key: _animatedListKey,
itemBuilder: _buildItem,
initialItemCount: _list.length,
scrollDirection: widget.scrollDirection,
reverse: widget.reverse,
controller: widget.controller,
primary: widget.primary,
physics: widget.physics,
shrinkWrap: widget.shrinkWrap,
padding: widget.padding,
);
}
Widget _buildItem(BuildContext context, int index, Animation<double> animation) {
return widget.itemBuilder(context, _list[index], animation, index);
}
}
typedef FirestoreListOnInit = void Function();
typedef FirestoreListOnChild = void Function(int index, DocumentSnapshot change);
typedef FirestoreListOnError = void Function(dynamic error, StackTrace trace);
class FirestoreList extends ListBase<DocumentSnapshot> {
FirestoreList({
@required this.query,
this.onInitialData,
this.onChildAdded,
this.onChildRemoved,
this.onChildChanged,
this.onError,
});
final Query query;
final FirestoreListOnInit onInitialData;
final FirestoreListOnChild onChildAdded;
final FirestoreListOnChild onChildRemoved;
final FirestoreListOnChild onChildChanged;
final FirestoreListOnError onError;
final List<DocumentSnapshot> _snapshots = <DocumentSnapshot>[];
StreamSubscription<QuerySnapshot> _sub;
bool _loaded = false;
void listen() {
_loaded = false;
_sub = query.snapshots().listen(_onData, onError: _onError);
}
void dispose() {
_sub?.cancel();
}
void _onData(QuerySnapshot snapshot) {
if (_loaded == false) {
if (_snapshots.length == snapshot.documents.length) {
_snapshots.setAll(0, snapshot.documents);
} else {
_snapshots.clear();
_snapshots.addAll(snapshot.documents);
}
_loaded = true;
onInitialData?.call();
return;
}
for (final change in snapshot.documentChanges) {
switch (change.type) {
case DocumentChangeType.added:
_snapshots.insert(change.newIndex, change.document);
onChildAdded?.call(change.newIndex, change.document);
break;
case DocumentChangeType.modified:
//print('modified ${change.oldIndex} ${change.newIndex}');
if (change.oldIndex == change.newIndex) {
_snapshots.removeAt(change.oldIndex);
_snapshots.insert(change.newIndex, change.document);
onChildChanged?.call(change.newIndex, change.document);
} else {
final oldDoc = _snapshots.removeAt(change.oldIndex);
onChildRemoved.call(change.oldIndex, oldDoc);
_snapshots.insert(change.newIndex, change.document);
onChildAdded?.call(change.newIndex, change.document);
}
break;
case DocumentChangeType.removed:
_snapshots.removeAt(change.oldIndex);
onChildRemoved.call(change.oldIndex, change.document);
break;
}
}
}
void _onError(error, StackTrace stackTrace) {
onError?.call(error, stackTrace);
}
bool get isLoaded => _loaded;
// ---
@override
DocumentSnapshot operator [](int index) => _snapshots[index];
@override
void operator []=(int index, DocumentSnapshot value) {
throw new UnsupportedError("List cannot be modified.");
}
@override
set length(int newLength) {
throw new UnsupportedError("List cannot be modified.");
}
@override
int get length => _snapshots.length;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment