Call function from Stateful Widget from another StatefulWidget - flutter

So basically I have a ProfilePage which is a StatelessWidget and inside of it's build method I display a form called MyForm which is a StatefulWidget and a widget called FancyFab which is another StatefulWidget.
Here's an example of how they are displayed on the parent widget:
#override
Widget build(BuildContext globalContext) {
return Scaffold(
appBar: AppBar(title: Text('Profile')),
floatingActionButton: FancyFab(),
body: Center(
child: FutureBuilder(
future: initData(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return MyForm(data: snapshot.data);
} else if (snapshot.hasError) {
print(snapshot.error);
return new Container(width: 0.0, height: 0.0);
} else {
return Center(child: CircularProgressIndicator());
}
},
)
)
);
}
My problem lies in that I have a saveData() function in MyFormState which grabs values from each TextFormField controller and saves it on the database. I need to call this function from within my FancyFab widget but I can't find a proper way to do so. Or even access those TextFormField controllers from within my FancyFab widget. Any help or guidance will be greatly appreciated.
EDIT
Here's how I implemented the FancyFab widget:
class FancyFab extends StatefulWidget {
final String tooltip;
final IconData icon;
String photo;
TextEditingController birthController;
TextEditingController firstController;
TextEditingController lastController;
TextEditingController emailController;
TextEditingController phoneController;
TextEditingController associationController;
TextEditingController countryController;
final Function saveData;
FancyFab({
this.tooltip,
this.icon,
this.saveData,
this.firstController,
this.lastController,
this.emailController,
this.phoneController,
this.associationController,
this.countryController,
this.birthController,
this.photo});
#override
_FancyFabState createState() => _FancyFabState();
}
class _FancyFabState extends State<FancyFab>
with SingleTickerProviderStateMixin {
bool isOpened = false;
AnimationController _animationController;
Animation<Color> _buttonColor;
Animation<double> _animateIcon;
Animation<double> _translateButton;
Curve _curve = Curves.easeOut;
double _fabHeight = 56.0;
#override
initState() {
_animationController =
AnimationController(vsync: this, duration: Duration(milliseconds: 500))
..addListener(() {
setState(() {});
});
_animateIcon =
Tween<double>(begin: 0.0, end: 1.0).animate(_animationController);
_buttonColor = ColorTween(
begin: Colors.pink,
end: Colors.red,
).animate(CurvedAnimation(
parent: _animationController,
curve: Interval(
0.00,
1.00,
curve: Curves.linear,
),
));
_translateButton = Tween<double>(
begin: _fabHeight,
end: -14.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: Interval(
0.0,
0.75,
curve: _curve,
),
));
super.initState();
}
#override
dispose() {
_animationController.dispose();
super.dispose();
}
animate() {
if (!isOpened) {
_animationController.forward();
} else {
_animationController.reverse();
}
isOpened = !isOpened;
}
Widget save() {
return Container(
child: FloatingActionButton(
heroTag: 'saveBtn',
onPressed: () {
},
tooltip: 'Save',
child: Icon(Icons.save),
),
);
}
Widget image() {
return Container(
child: FloatingActionButton(
heroTag: 'imageBtn',
onPressed: () async {
File file = await FilePicker.getFile(type: FileType.IMAGE);
AlertDialog alert = AlertDialog(
title: Text('Uploading photo'),
titlePadding: EdgeInsets.all(20.0),
contentPadding: EdgeInsets.all(20.0),
elevation:10,
content: CircularProgressIndicator(),
);
final StorageReference storageRef = FirebaseStorage.instance.ref().child(file.path);
final String fileName = file.path;
final StorageUploadTask uploadTask = storageRef.putFile(
File(fileName),
);
final StorageTaskSnapshot downloadUrl =
(await uploadTask.onComplete);
final String url = (await downloadUrl.ref.getDownloadURL());
print('URL Is $url');
setState(() {
widget.photo = url;
});
},
tooltip: 'Image',
child: Icon(Icons.image),
),
);
}
Widget toggle() {
return Container(
child: FloatingActionButton(
backgroundColor: _buttonColor.value,
onPressed: animate,
heroTag: 'toggleBtn',
tooltip: 'Toggle',
child: AnimatedIcon(
icon: AnimatedIcons.menu_close,
progress: _animateIcon,
),
),
);
}
#override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Transform(
transform: Matrix4.translationValues(
0.0,
_translateButton.value * 2.0,
0.0,
),
child: save(),
),
Transform(
transform: Matrix4.translationValues(
0.0,
_translateButton.value * 1.0,
0.0,
),
child: image(),
),
toggle(),
],
);
}
}

As far as I can see, there is a slight issue with the data-flow of the application, for example, strictly from a fucntionality based observation, the data and the trigger (FancyFab) should be in the same widget or in an directly accessible widget. But nevertheless if you want to continue, here is a very primitive (VERY) way to do it.

I managed to solve this by nesting a Scaffold inside MyForm widget. Then the tree changed and my FancyFab widget was now a child from MyForm widget. Next I passed parent's saveData() function and all my controllers to my child widget and that way I was able to call parent function providing the appropiate data. The fix was a follows:
Solution
Parent widget (MyForm)
class MyFormState extends State<MyForm> {
User user;
TextEditingController birthController = TextEditingController();
TextEditingController firstController = TextEditingController();
TextEditingController lastController = TextEditingController();
TextEditingController emailController = TextEditingController();
TextEditingController phoneController = TextEditingController();
TextEditingController associationController = TextEditingController();
TextEditingController countryController = TextEditingController();
#override
void initState() {
super.initState();
user = widget.data[1];
firstController.text = user.firstName;
lastController.text = user.lastName;
countryController.text = user.country;
birthController.text = user.dateBirth;
emailController.text = user.email;
phoneController.text = user.phone;
associationController.text = user.association;
}
_updatePhoto(String text) {
setState(() {
user.photo = text;
});
}
_saveData(data, BuildContext ctx) async {
FirebaseUser fireUser= await FirebaseAuth.instance.currentUser();
var uid = fireUser.uid;
await Firestore.instance.collection('users').document(uid).updateData(data).then((val) {
AlertDialog alert = AlertDialog(
title: Text('Confirmation'),
titlePadding: EdgeInsets.all(20.0),
// contentPadding: EdgeInsets.all(20.0),
elevation:10,
content: Text('Your profile has been saved.')
);
showDialog(context: ctx,
builder: (BuildContext context){
return alert;
});
})
.catchError((err) {
final snackBar = SnackBar(
content: Text('There have been an error updating your profile' + err.toString()),
elevation: 10,
);
Scaffold.of(ctx).showSnackBar(snackBar);
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FancyFab(
parentAction: _updatePhoto,
saveData: _saveData,
firstController: firstController,
lastController: lastController,
emailController: emailController,
countryController: countryController,
associationController: associationController,
phoneController: phoneController,
birthController: birthController,
photo: user.photo),
body:Form(
//more UI
)
);
}
}
Child widget (FancyFab)
class FancyFab extends StatefulWidget {
final void Function(String value) parentAction;
final void Function(dynamic data, BuildContext ctx) saveData;
final String tooltip;
final IconData icon;
String photo;
TextEditingController birthController;
TextEditingController firstController;
TextEditingController lastController;
TextEditingController emailController;
TextEditingController phoneController;
TextEditingController associationController;
TextEditingController countryController;
FancyFab({
this.parentAction,
this.tooltip,
this.icon,
this.saveData,
this.firstController,
this.lastController,
this.emailController,
this.phoneController,
this.associationController,
this.countryController,
this.birthController,
this.photo});
#override
_FancyFabState createState() => _FancyFabState();
}
Then I could just call parent function using widget.parentFunction(data) on my child widget state.
Special thanks to #diegoveloper for providing with a great article explaining the types of widgets interactions.

Related

Rive's stateMachine with TextFiled

I Like to create animation that will follow the TextField text, here is video from
Flutter YouTube.
Now how can I follow the Target.
Here is My rive file on rive.app or GitHub and Design.
We need a StateMachineController and SMIInput<double> that will be responsible to follow the text.
Result
OutPutVideo
Result with Slider
You can follow the GitHub Repository or
class TextFieldWithRive extends StatefulWidget {
TextFieldWithRive({Key? key}) : super(key: key);
#override
_TextFieldWithRiveState createState() => _TextFieldWithRiveState();
}
class _TextFieldWithRiveState extends State<TextFieldWithRive> {
StateMachineController? controller;
SMIInput<double>? valueController;
Artboard? _riveArtboard;
double sliderVal = 0.0;
/// change value based on size>Width
final double strengthOverTextFiled = 1.5;
final TextEditingController textEditingController = TextEditingController();
#override
void initState() {
super.initState();
rootBundle.load("rives/eyeMovement.riv").then((value) async {
final file = RiveFile.import(value);
final artboard = file.mainArtboard;
controller = StateMachineController.fromArtboard(artboard, "eyeMovement");
if (controller != null) {
print("got state");
setState(() {
artboard.addController(controller!);
valueController = controller!.findInput('moevement_controll');
controller!.inputs.forEach((element) {
print(element.name);
});
});
}
_riveArtboard = artboard;
});
///* eye controll with textFiled
textEditingController.addListener(() {
print(textEditingController.text);
if (valueController != null) {
valueController!.value =
textEditingController.text.length * strengthOverTextFiled;
}
});
}
#override
void dispose() {
textEditingController.removeListener(() {});
textEditingController.dispose();
controller!.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: LayoutBuilder(
builder: (context, constraints) => Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
height: constraints.maxWidth * .5,
width: constraints.maxWidth * .5,
child: _riveArtboard == null
? CircularProgressIndicator()
: Rive(
artboard: _riveArtboard!,
),
),
SizedBox(
width: constraints.maxWidth * .8,
child: TextField(
controller: textEditingController,
decoration: InputDecoration(hintText: "keep typing"),
),
),
],
),
),
),
);
}
}

ValueKey does not work properly with the animatedbuilder - flutter

So, I am making the sport application with the bookmark articles functionality. The bookmark article widget has an animated icon with a simple size animation. After clicking on the icon that removes the article, the icons in other widgets refresh in a few moments. I put the link with the screen video below.
https://firebasestorage.googleapis.com/v0/b/footballapp-2fb40.appspot.com/o/Screen_Recording_20210727-122200.mp4?alt=media&token=794fb85b-659a-4127-a6cb-e784bde7ff72
class BookmarkArticleIcon extends StatefulWidget {
final Color iconColor;
final double iconSize;
final Article article;
final ValueKey key;
BookmarkArticleIcon({this.iconColor, this.iconSize, this.article, this.key})
: super(key: key);
#override
_BookmarkArticleIconState createState() => _BookmarkArticleIconState();
}
class _BookmarkArticleIconState extends State<BookmarkArticleIcon>
with SingleTickerProviderStateMixin {
AnimationController _animationController;
Animation<double> _animation;
#override
void initState() {
super.initState();
BlocProvider.of<BookmarkArticleBloc>(context)
.add(FetchInitialArticleState(article: widget.article));
_animationController =
AnimationController(vsync: this, duration: Duration(milliseconds: 100))
..addListener(() {
if (_animationController.isCompleted) {
_animationController.reverse();
}
});
_animation =
Tween<double>(begin: 1.0, end: 1.2).animate(_animationController);
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return BlocBuilder<BookmarkArticleBloc, BookmarkArticleState>(
buildWhen: _buildWhenFunction,
builder: (context, state) {
if (state is BookmarkArticleResult) {
return AnimatedBuilder(
animation: _animationController,
builder: (context, _) {
return Transform.scale(
scale: _animation.value,
child: InkWell(
child: Icon(
(state.isBookmarked)
? Icons.bookmark
: Icons.bookmark_outline,
color: widget.iconColor,
size: widget.iconSize,
),
onTap: () {
_onTapFunction(state);
}),
);
});
} else {
return LoadingWidget(
iconColor: widget.iconColor,
);
}
},
);
}
void _onTapFunction(BookmarkArticleResult state) {
final BookmarkArticleBloc _bookmarkArticleBloc =
BlocProvider.of<BookmarkArticleBloc>(context);
_animationController.forward();
if (state.isBookmarked) {
_bookmarkArticleBloc
.add(RemoveBookmarkedArticle(article: widget.article));
} else {
_bookmarkArticleBloc.add(AddBookmarkArticle(article: widget.article));
}
}
bool _buildWhenFunction(previous, current) {
if ((current is BookmarkArticleResult &&
current.articleID == widget.article.id) ||
(current is LoadingBookmarkArticle &&
current.articleID == widget.article.id)) {
return true;
} else {
return false;
}
}
}
class LoadingWidget extends StatelessWidget {
final Color iconColor;
LoadingWidget({this.iconColor});
#override
Widget build(BuildContext context) {
return Container(
key: UniqueKey(),
margin: EdgeInsets.all(4),
width: 16,
height: 16,
child: CircularProgressIndicator(
color: iconColor,
strokeWidth: 2.0,
),
);
}
}

How to correctly start and end or animation and changing text from other class flutter

I am learning flutter and i want to start animation & set App bar title 'syncing' from AlertDialog response(basically from other class) then end animation & set Title again after Async operation.
So currently I am achieving this using GlobalKey and Riverpod(StateNotifier).
Created MainScreen GlobalKey and using that GlobalKey from other class before Async Operation i am Calling
mainScreenScaffoldKey.currentState.context
.read(syncProgressProvider)
.setSyncing();
and ending Animation after async operation:
mainScreenScaffoldKey.currentState.context
.read(syncProgressProvider)
.syncProgressDone();
code :
Map<String, dynamic> dialogResponse = await showDialog(
context: context,
builder: (context) => EditNoteScreen(
_index,
_task,
_color,
dateTime,
priority: priority,
));
if (dialogResponse != null) {
mainScreenScaffoldKey.currentState.context
.read(syncProgressProvider)
.setSyncing();
await SaveToLocal().save(context.read(listStateProvider.state));
await CloudNotes().updateCloudNote(
task: dialogResponse["task"],
priority: dialogResponse["priority"],
dateTime: dateTime.toString(),
index: dialogResponse["index"],
);
mainScreenScaffoldKey.currentState.context
.read(syncProgressProvider)
.syncProgressDone();
}
and listening variable in AppBar title property in MainScreen
I feel this not right approach or is it?
here are some extra snippet
syncProgressProiver:
class SyncProgressModel extends StateNotifier<bool>{
SyncProgressModel() : super(false);
syncProgressDone(){
state =false;
}
setSyncing(){
state =true;
}
MainScreen AppBar Title
Consumer(
builder: (context, watch, child) {
var syncProgress = watch(syncProgressProvider.state);
if (!syncProgress) {
return const Text('To-Do List');
} else {
return Row(
children: [
const Text('Syncing..'),
Container(
margin: const EdgeInsets.only(left: 10),
width: 25,
height: 25,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: animColors,
),
)
],
);
}
},
),
Like this
I don't know anything about your animations (you don't actually share any of that logic or what initState are you referring to) but if the only thing you want is to animate the color of the CircularProgressIndicator then you could just create a StatefulWidget that does that for you and call it to build only when syncProgress == true
class AnimatedWidget extends StatefulWidget {
AnimatedWidget({Key key}) : super(key: key);
#override
_AnimatedWidgetState createState() => _AnimatedWidgetState();
}
class _AnimatedWidgetState extends State<AnimatedWidget>
with SingleTickerProviderStateMixin {
AnimationController _controller;
final Animatable<Color> _colorTween = TweenSequence<Color>([
TweenSequenceItem<Color>(
tween: ColorTween(begin: Colors.red, end: Colors.amber),
weight: 20,
),
TweenSequenceItem<Color>(
tween: ColorTween(begin: Colors.amber, end: Colors.green),
weight: 20,
),
TweenSequenceItem<Color>(
tween: ColorTween(begin: Colors.green, end: Colors.blue),
weight: 20,
),
TweenSequenceItem<Color>(
tween: ColorTween(begin: Colors.blue, end: Colors.purple),
weight: 20,
),
TweenSequenceItem<Color>(
tween: ColorTween(begin: Colors.purple, end: Colors.red),
weight: 20,
),
]).chain(CurveTween(curve: Curves.linear));
#override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 5),
animationBehavior: AnimationBehavior.preserve,
vsync: this,
)..repeat();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Row(
children: [
const Text('Syncing..'),
Container(
margin: const EdgeInsets.only(left: 10),
width: 25,
height: 25,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: _colorTween.animate(_controller)
),
)
],
);
}
}
and in your consumer just call it, the widget will handle the animation itself
Consumer(
builder: (context, watch, child) {
var syncProgress = watch(syncProgressProvider.state);
if (!syncProgress) {
return const Text('To-Do List');
} else {
return AnimatedWidget(); //right here
}
},
),
UPDATE
To safely refer to a widget's ancestor in its dispose() method, save a
reference to the ancestor by calling
dependOnInheritedWidgetOfExactType() in the widget's
didChangeDependencies() method.
What does this means is that you should keep a reference of the objects depending on your context before the context itself is rebuilt (when you call updateValueAt or updateValue in your dialog it rebuild the list and is no longer safe to call context.read)
updateCloudNote(BuildContext context) async {
/// keep the reference before calling the dialog to prevent
/// when the context change because of the dialogs action
final syncProgress = context.read(syncProgressProvider);
final listState = context.read(listStateProvider.state);
Map<String, dynamic> dialogResponse = await showDialog(
context: context,
builder: (context) => EditNoteScreen(
_index,
_task,
_color,
dateTime,
priority: priority,
));
if (dialogResponse != null) {
//when using GlobalKey didn't get that Widget Ancestor error
// use the reference saved instead of context.read
syncProgress.setSyncing();
await SaveToLocal().save(listState);
await CloudNotes().updateCloudNote(
task: dialogResponse["task"],
priority: dialogResponse["priority"],
dateTime: dateTime.toString(),
index: dialogResponse["index"],
);
syncProgress.syncProgressDone();
}
}
What you did combining the providers is basically what you could do inside the same syncProgressProvider
final syncProgressProvider = StateNotifierProvider<SyncProgressModel>((ref)
=> SyncProgressModel(ref.read));
class SyncProgressModel extends StateNotifier<bool>{
final saveToLocal saveLocal = SaveToLocal();
final CloudNotes cloudNotes = CloudNotes();
final Reader _read;
SyncProgressModel(this._read) : super(false);
syncProgressDone(){
state = false;
}
setSyncing(){
state = true;
}
updateCall({int priority, int index, String task, String dateTime}) async {
state = true;
await saveLocal.save(_read(listStateProvider.state));
await cloudNotes.updateCloudNote(
task: task,
priority: priority,
dateTime: dateTime,
index: index,
);
state = false;
}
}
And finally combining the 2 ideas:
updateCloudNote(BuildContext context) async {
/// keep the reference before calling the dialog to prevent
/// when the context change because of the dialogs action
final syncProgress = context.read(syncProgressProvider);
Map<String, dynamic> dialogResponse = await showDialog(
context: context,
builder: (context) => EditNoteScreen(
_index,
_task,
_color,
dateTime,
priority: priority,
));
if (dialogResponse != null) {
await syncProgress.updateCall(
task: dialogResponse["task"],
priority: dialogResponse["priority"],
dateTime: dateTime.toString(),
index: dialogResponse["index"],
);
}
}

How to truncate text and make left side of text show in a textField

I have a textfield (see image) that when a user selects an address it is copied into the textfield. The issue is, even when the textfield loses focus, it is that you cannot see the beginning of the text. In this example, the street number is to the left and cut off. I want the textfield, when losing focus or when text is set programmatically, to show the far left side of the text, not the right side. I am guessing the cursor is at the end. But when losing focus there is no cursor, a textfield below has focus.
I am using AutoComplete package. But here is the code.
library auto_complete_text_view;
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
typedef void OnTapCallback(String value);
class AutoCompleteTextView extends StatefulWidget
with AutoCompleteTextInterface {
final double maxHeight;
final TextEditingController controller;
//AutoCompleteTextField properties
final tfCursorColor;
final tfCursorWidth;
final tfStyle;
final tfTextDecoration;
final tfTextAlign;
//Suggestiondrop Down properties
final suggestionStyle;
final suggestionTextAlign;
final onTapCallback;
final Function getSuggestionsMethod;
final Function focusGained;
final Function focusLost;
final int suggestionsApiFetchDelay;
final Function onValueChanged;
AutoCompleteTextView(
{#required this.controller,
this.onTapCallback,
this.maxHeight = 0,
this.tfCursorColor = Colors.white,
this.tfCursorWidth = 2.0,
this.tfStyle = const TextStyle(color: Colors.black),
this.tfTextDecoration = const InputDecoration(),
this.tfTextAlign = TextAlign.left,
this.suggestionStyle = const TextStyle(color: Colors.black),
this.suggestionTextAlign = TextAlign.left,
#required this.getSuggestionsMethod,
this.focusGained,
this.suggestionsApiFetchDelay = 0,
this.focusLost,
this.onValueChanged});
#override
_AutoCompleteTextViewState createState() => _AutoCompleteTextViewState();
//This funciton is called when a user clicks on a suggestion
#override
void onTappedSuggestion(String suggestion) {
onTapCallback(suggestion);
}
}
class _AutoCompleteTextViewState extends State<AutoCompleteTextView> {
ScrollController scrollController = ScrollController();
FocusNode _focusNode = FocusNode();
OverlayEntry _overlayEntry;
LayerLink _layerLink = LayerLink();
final suggestionsStreamController = new BehaviorSubject<List<String>>();
List<String> suggestionShowList = List<String>();
Timer _debounce;
bool isSearching = true;
#override
void initState() {
super.initState();
_focusNode.addListener(() {
if (_focusNode.hasFocus) {
this._overlayEntry = this._createOverlayEntry();
Overlay.of(context).insert(this._overlayEntry);
(widget.focusGained != null) ? widget.focusGained() : () {};
} else {
this._overlayEntry.remove();
(widget.focusLost != null) ? widget.focusLost() : () {};
}
});
widget.controller.addListener(_onSearchChanged);
}
_onSearchChanged() {
if (_debounce?.isActive ?? false) _debounce.cancel();
_debounce =
Timer(Duration(milliseconds: widget.suggestionsApiFetchDelay), () {
if (isSearching == true) {
_getSuggestions(widget.controller.text);
}
});
}
_getSuggestions(String data) async {
if (data.length > 0 && data != null) {
List<String> list = await widget.getSuggestionsMethod(data);
suggestionsStreamController.sink.add(list);
}
}
OverlayEntry _createOverlayEntry() {
RenderBox renderBox = context.findRenderObject();
var size = renderBox.size;
return OverlayEntry(
builder: (context) => Positioned(
width: size.width,
child: CompositedTransformFollower(
link: this._layerLink,
showWhenUnlinked: false,
offset: Offset(0.0, size.height + 5.0),
child: Material(
elevation: 4.0,
child: StreamBuilder<Object>(
stream: suggestionsStreamController.stream,
builder: (context, suggestionData) {
if (suggestionData.hasData &&
widget.controller.text.isNotEmpty) {
suggestionShowList = suggestionData.data;
return ConstrainedBox(
constraints: new BoxConstraints(
maxHeight: 0,
),
child: ListView.builder(
controller: scrollController,
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: suggestionShowList.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(
suggestionShowList[index],
style: widget.suggestionStyle,
textAlign: widget.suggestionTextAlign,
),
onTap: () {
isSearching = false;
widget.controller.text =
suggestionShowList[index];
suggestionsStreamController.sink.add([]);
widget.onTappedSuggestion(
widget.controller.text);
},
);
}),
);
} else {
return Container();
}
}),
),
),
));
}
#override
Widget build(BuildContext context) {
return CompositedTransformTarget(
link: this._layerLink,
child: TextField(
controller: widget.controller,
decoration: widget.tfTextDecoration,
style: widget.tfStyle,
cursorColor: widget.tfCursorColor,
cursorWidth: widget.tfCursorWidth,
textAlign: widget.tfTextAlign,
focusNode: this._focusNode,
onChanged: (text) {
if (text.trim().isNotEmpty) {
(widget.onValueChanged != null)
? widget.onValueChanged(text)
: () {};
isSearching = true;
scrollController.animateTo(
0.0,
curve: Curves.easeOut,
duration: const Duration(milliseconds: 300),
);
} else {
isSearching = false;
suggestionsStreamController.sink.add([]);
}
},
),
);
}
#override
void dispose() {
suggestionsStreamController.close();
scrollController.dispose();
widget.controller.dispose();
super.dispose();
}
}
abstract class AutoCompleteTextInterface {
void onTappedSuggestion(String suggestion);
}
Using a ScrollController we can make the text on the far left side visible.
Create and initialize final scrollController = ScrollController();.
Create a method for showing left side text contents, method contains scrollController.jumpTo(0.0);.
In the TextField add scrollController: property and set its value to scrollController from step 1.
In the TextField add onEditingComplete: property and set its value to method created in step 2.
Made a Small Code sample for the above mentioned points: pastebin
Note:
This code does not move the cursor. It only makes the left side visible.
This code relies on the onEditingComplete callback, in different situations you may need to use another callback.

Pagination for Firestore animated list flutter

I'm making a chat app that messages should be shown on screen with nice animation and my backend is Firestore, so I decided to use this (https://pub.dev/packages/firestore_ui) plugin for animating messages.
Now I want to implement pagination to prevent expensive works and bills.
Is there any way?
How should I implement it?
main problem is making a firestore animated list with pagination,
It's easy to make simple ListView with pagination.
as you can see in below code, this plugin uses Query of snapShots to show incoming messages (documents) with animation:
FirestoreAnimatedList(
query: query,
itemBuilder: (
BuildContext context,
DocumentSnapshot snapshot,
Animation<double> animation,
int index,
) => FadeTransition(
opacity: animation,
child: MessageListTile(
index: index,
document: snapshot,
onTap: _removeMessage,
),
),
);
if we want to use AnimatedList widget instead, we will have problem because we should track realtime messages(documents) that are adding to our collection.
I put together an example for you: https://gist.github.com/slightfoot/d936391bfb77a5301335c12e3e8861de
// MIT License
//
// Copyright (c) 2020 Simon Lightfoot
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import 'dart:async';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart' show ScrollDirection;
import 'package:provider/provider.dart';
///
/// Firestore Chat List Example - by Simon Lightfoot
///
/// Setup instructions:
///
/// 1. Create project on console.firebase.google.com.
/// 2. Add firebase_auth package to your pubspec.yaml.
/// 3. Add cloud_firestore package to your pubspec.yaml.
/// 4. Follow the steps to add firebase to your application on Android/iOS.
/// 5. Go to the authentication section of the firebase console and enable
/// anonymous auth.
///
/// Now run the example on two or more devices and start chatting.
///
///
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final user = await FirebaseAuth.instance.currentUser();
runApp(ExampleChatApp(user: user));
}
class ExampleChatApp extends StatefulWidget {
const ExampleChatApp({
Key key,
this.user,
}) : super(key: key);
final FirebaseUser user;
static Future<FirebaseUser> signIn(BuildContext context, String displayName) {
final state = context.findAncestorStateOfType<_ExampleChatAppState>();
return state.signIn(displayName);
}
static Future<void> postMessage(ChatMessage message) async {
await Firestore.instance
.collection('messages')
.document()
.setData(message.toJson());
}
static Future<void> signOut(BuildContext context) {
final state = context.findAncestorStateOfType<_ExampleChatAppState>();
return state.signOut();
}
#override
_ExampleChatAppState createState() => _ExampleChatAppState();
}
class _ExampleChatAppState extends State<ExampleChatApp> {
StreamSubscription<FirebaseUser> _userSub;
FirebaseUser _user;
Future<FirebaseUser> signIn(String displayName) async {
final result = await FirebaseAuth.instance.signInAnonymously();
await result.user.updateProfile(
UserUpdateInfo()..displayName = displayName,
);
final user = await FirebaseAuth.instance.currentUser();
setState(() => _user = user);
return user;
}
Future<void> signOut() {
return FirebaseAuth.instance.signOut();
}
#override
void initState() {
super.initState();
_user = widget.user;
_userSub = FirebaseAuth.instance.onAuthStateChanged.listen((user) {
print('changed ${user?.uid} -> ${user?.displayName}');
setState(() => _user = user);
});
}
#override
void dispose() {
_userSub.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Provider<FirebaseUser>.value(
value: _user,
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Firestore Chat List',
home: _user == null ? LoginScreen() : ChatScreen(),
),
);
}
}
class LoginScreen extends StatefulWidget {
static Route<dynamic> route() {
return MaterialPageRoute(
builder: (BuildContext context) {
return LoginScreen();
},
);
}
#override
_LoginScreenState createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
TextEditingController _displayName;
bool _loading = false;
#override
void initState() {
super.initState();
_displayName = TextEditingController();
}
#override
void dispose() {
_displayName.dispose();
super.dispose();
}
Future<void> _onSubmitPressed() async {
setState(() => _loading = true);
try {
final user = await ExampleChatApp.signIn(context, _displayName.text);
if (mounted) {
await ExampleChatApp.postMessage(
ChatMessage.notice(user, 'has entered the chat'));
Navigator.of(context).pushReplacement(ChatScreen.route());
}
} finally {
if (mounted) {
setState(() => _loading = false);
}
}
}
#override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: Text('Firestore Chat List'),
),
body: SizedBox.expand(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Login',
style: theme.textTheme.headline4,
textAlign: TextAlign.center,
),
SizedBox(height: 32.0),
if (_loading)
CircularProgressIndicator()
else ...[
TextField(
controller: _displayName,
decoration: InputDecoration(
hintText: 'Display Name',
border: OutlineInputBorder(),
isDense: true,
),
onSubmitted: (_) => _onSubmitPressed(),
textInputAction: TextInputAction.go,
),
SizedBox(height: 12.0),
RaisedButton(
onPressed: () => _onSubmitPressed(),
child: Text('ENTER CHAT'),
),
],
],
),
),
),
);
}
}
class ChatScreen extends StatelessWidget {
static Route<dynamic> route() {
return MaterialPageRoute(
builder: (BuildContext context) {
return ChatScreen();
},
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Firestore Chat List'),
actions: [
IconButton(
onPressed: () async {
final user = Provider.of<FirebaseUser>(context, listen: false);
ExampleChatApp.postMessage(
ChatMessage.notice(user, 'has left the chat.'));
Navigator.of(context).pushReplacement(LoginScreen.route());
await ExampleChatApp.signOut(context);
},
icon: Icon(Icons.exit_to_app),
),
],
),
body: Column(
children: [
Expanded(
child: FirestoreChatList(
listenBuilder: () {
return Firestore.instance
.collection('messages')
.orderBy('posted', descending: true);
},
pagedBuilder: () {
return Firestore.instance
.collection('messages')
.orderBy('posted', descending: true)
.limit(15);
},
itemBuilder: (BuildContext context, int index,
DocumentSnapshot document, Animation<double> animation) {
final message = ChatMessage.fromDoc(document);
return SizeTransition(
key: Key('message-${document.documentID}'),
axis: Axis.vertical,
axisAlignment: -1.0,
sizeFactor: animation,
child: Builder(
builder: (BuildContext context) {
switch (message.type) {
case ChatMessageType.notice:
return ChatMessageNotice(message: message);
case ChatMessageType.text:
return ChatMessageBubble(message: message);
}
throw StateError('Bad message type');
},
),
);
},
),
),
SendMessagePanel(),
],
),
);
}
}
class ChatMessageNotice extends StatelessWidget {
const ChatMessageNotice({
Key key,
#required this.message,
}) : super(key: key);
final ChatMessage message;
#override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(24.0),
alignment: Alignment.center,
child: Text(
'${message.displayName} ${message.message}',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.grey.shade700,
fontStyle: FontStyle.italic,
),
),
);
}
}
class ChatMessageBubble extends StatelessWidget {
const ChatMessageBubble({
Key key,
#required this.message,
}) : super(key: key);
final ChatMessage message;
MaterialColor _calculateUserColor(String uid) {
final hash = uid.codeUnits.fold(0, (prev, el) => prev + el);
return Colors.primaries[hash % Colors.primaries.length];
}
#override
Widget build(BuildContext context) {
final isMine = message.isMine(context);
return Container(
padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
width: double.infinity,
child: Column(
crossAxisAlignment:
isMine ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [
FractionallySizedBox(
widthFactor: 0.6,
child: Container(
decoration: BoxDecoration(
color: _calculateUserColor(message.uid).shade200,
borderRadius: isMine
? const BorderRadius.only(
topLeft: Radius.circular(24.0),
topRight: Radius.circular(24.0),
bottomLeft: Radius.circular(24.0),
)
: const BorderRadius.only(
topLeft: Radius.circular(24.0),
topRight: Radius.circular(24.0),
bottomRight: Radius.circular(24.0),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (message.displayName?.isNotEmpty ?? false) ...[
const SizedBox(width: 8.0),
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _calculateUserColor(message.uid),
),
padding: EdgeInsets.all(8.0),
child: Text(
message.displayName.substring(0, 1),
style: TextStyle(
color: Colors.white,
fontSize: 24.0,
),
),
),
],
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(message.message),
),
),
],
),
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Text(
message.infoText(context),
style: TextStyle(
fontSize: 12.0,
color: Colors.grey.shade600,
),
),
),
],
),
);
}
}
class SendMessagePanel extends StatefulWidget {
#override
_SendMessagePanelState createState() => _SendMessagePanelState();
}
class _SendMessagePanelState extends State<SendMessagePanel> {
final _controller = TextEditingController();
FirebaseUser _user;
#override
void didChangeDependencies() {
super.didChangeDependencies();
_user = Provider.of<FirebaseUser>(context);
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
void _onSubmitPressed() {
if (_controller.text.isEmpty) {
return;
}
ExampleChatApp.postMessage(ChatMessage.text(_user, _controller.text));
_controller.clear();
}
#override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: Colors.grey.shade200,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
offset: Offset(0.0, -3.0),
blurRadius: 4.0,
spreadRadius: 3.0,
)
],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: 160.0),
child: TextField(
controller: _controller,
decoration: InputDecoration(
border: OutlineInputBorder(),
filled: true,
fillColor: Colors.grey.shade300,
isDense: true,
),
onSubmitted: (_) => _onSubmitPressed(),
maxLines: null,
textInputAction: TextInputAction.send,
),
),
),
IconButton(
onPressed: () => _onSubmitPressed(),
icon: Icon(Icons.send),
),
],
),
);
}
}
enum ChatMessageType {
notice,
text,
}
class ChatMessage {
const ChatMessage._({
this.type,
this.posted,
this.message = '',
this.uid,
this.displayName,
this.photoUrl,
}) : assert(type != null && posted != null);
final ChatMessageType type;
final DateTime posted;
final String message;
final String uid;
final String displayName;
final String photoUrl;
String infoText(BuildContext context) {
final timeOfDay = TimeOfDay.fromDateTime(posted);
final localizations = MaterialLocalizations.of(context);
final date = localizations.formatShortDate(posted);
final time = localizations.formatTimeOfDay(timeOfDay);
return '$date at $time from $displayName';
}
bool isMine(BuildContext context) {
final user = Provider.of<FirebaseUser>(context);
return uid == user?.uid;
}
factory ChatMessage.notice(FirebaseUser user, String message) {
return ChatMessage._(
type: ChatMessageType.notice,
posted: DateTime.now().toUtc(),
message: message,
uid: user.uid,
displayName: user.displayName,
photoUrl: user.photoUrl,
);
}
factory ChatMessage.text(FirebaseUser user, String message) {
return ChatMessage._(
type: ChatMessageType.text,
posted: DateTime.now().toUtc(),
message: message,
uid: user.uid,
displayName: user.displayName,
photoUrl: user.photoUrl,
);
}
factory ChatMessage.fromDoc(DocumentSnapshot doc) {
return ChatMessage._(
type: ChatMessageType.values[doc['type'] as int],
posted: (doc['posted'] as Timestamp).toDate(),
message: doc['message'] as String,
uid: doc['user']['uid'] as String,
displayName: doc['user']['displayName'] as String,
photoUrl: doc['user']['photoUrl'] as String,
);
}
Map<String, dynamic> toJson() {
return {
'type': type.index,
'posted': Timestamp.fromDate(posted),
'message': message,
'user': {
'uid': uid,
'displayName': displayName,
'photoUrl': photoUrl,
},
};
}
}
// ---- CHAT LIST IMPLEMENTATION ----
typedef Query FirestoreChatListQueryBuilder();
typedef Widget FirestoreChatListItemBuilder(
BuildContext context,
int index,
DocumentSnapshot document,
Animation<double> animation,
);
typedef Widget FirestoreChatListLoaderBuilder(
BuildContext context,
int index,
Animation<double> animation,
);
class FirestoreChatList extends StatefulWidget {
const FirestoreChatList({
Key key,
this.controller,
#required this.listenBuilder,
#required this.pagedBuilder,
#required this.itemBuilder,
this.loaderBuilder = defaultLoaderBuilder,
this.scrollDirection = Axis.vertical,
this.reverse = true,
this.primary,
this.physics,
this.shrinkWrap = false,
this.initialAnimate = false,
this.padding,
this.duration = const Duration(milliseconds: 300),
}) : super(key: key);
final FirestoreChatListQueryBuilder listenBuilder;
final FirestoreChatListQueryBuilder pagedBuilder;
final FirestoreChatListItemBuilder itemBuilder;
final FirestoreChatListLoaderBuilder loaderBuilder;
final ScrollController controller;
final Axis scrollDirection;
final bool reverse;
final bool primary;
final ScrollPhysics physics;
final bool shrinkWrap;
final bool initialAnimate;
final EdgeInsetsGeometry padding;
final Duration duration;
static Widget defaultLoaderBuilder(
BuildContext context, int index, Animation<double> animation) {
return FadeTransition(
opacity: animation,
child: Container(
padding: EdgeInsets.all(32.0),
alignment: Alignment.center,
child: CircularProgressIndicator(),
),
);
}
#override
_FirestoreChatListState createState() => _FirestoreChatListState();
}
class _FirestoreChatListState extends State<FirestoreChatList> {
final _animatedListKey = GlobalKey<AnimatedListState>();
final _dataListen = List<DocumentSnapshot>();
final _dataPaged = List<DocumentSnapshot>();
Future _pageRequest;
StreamSubscription<QuerySnapshot> _listenSub;
ScrollController _controller;
ScrollController get controller =>
widget.controller ?? (_controller ??= ScrollController());
#override
void initState() {
super.initState();
controller.addListener(_onScrollChanged);
_requestNextPage();
}
#override
void dispose() {
controller.removeListener(_onScrollChanged);
_controller?.dispose();
_listenSub?.cancel();
super.dispose();
}
void _onScrollChanged() {
if (!controller.hasClients) {
return;
}
final position = controller.position;
if ((position.pixels >=
(position.maxScrollExtent - position.viewportDimension)) &&
position.userScrollDirection == ScrollDirection.reverse) {
_requestNextPage();
}
}
void _requestNextPage() {
_pageRequest ??= () async {
final loaderIndex = _addLoader();
// await Future.delayed(const Duration(seconds: 3));
var pagedQuery = widget.pagedBuilder();
if (_dataPaged.isNotEmpty) {
pagedQuery = pagedQuery.startAfterDocument(_dataPaged.last);
}
final snapshot = await pagedQuery.getDocuments();
if (!mounted) {
return;
}
final insertIndex = _dataListen.length + _dataPaged.length;
_dataPaged.addAll(snapshot.documents);
_removeLoader(loaderIndex);
for (int i = 0; i < snapshot.documents.length; i++) {
_animateAdded(insertIndex + i);
}
if (_listenSub == null) {
var listenQuery = widget.listenBuilder();
if (_dataPaged.isNotEmpty) {
listenQuery = listenQuery.endBeforeDocument(_dataPaged.first);
}
_listenSub = listenQuery.snapshots().listen(_onListenChanged);
}
_pageRequest = null;
}();
}
void _onListenChanged(QuerySnapshot snapshot) {
for (final change in snapshot.documentChanges) {
switch (change.type) {
case DocumentChangeType.added:
_dataListen.insert(change.newIndex, change.document);
_animateAdded(change.newIndex);
break;
case DocumentChangeType.modified:
if (change.oldIndex == change.newIndex) {
_dataListen.removeAt(change.oldIndex);
_dataListen.insert(change.newIndex, change.document);
setState(() {});
} else {
final oldDoc = _dataListen.removeAt(change.oldIndex);
_animateRemoved(change.oldIndex, oldDoc);
_dataListen.insert(change.newIndex, change.document);
_animateAdded(change.newIndex);
}
break;
case DocumentChangeType.removed:
final oldDoc = _dataListen.removeAt(change.oldIndex);
_animateRemoved(change.oldIndex, oldDoc);
break;
}
}
}
int _addLoader() {
final index = _dataListen.length + _dataPaged.length;
_animatedListKey?.currentState
?.insertItem(index, duration: widget.duration);
return index;
}
void _removeLoader(int index) {
_animatedListKey?.currentState?.removeItem(
index,
(BuildContext context, Animation<double> animation) {
return widget.loaderBuilder(context, index, animation);
},
duration: widget.duration,
);
}
void _animateAdded(int index) {
final animatedListState = _animatedListKey.currentState;
if (animatedListState != null) {
animatedListState.insertItem(index, duration: widget.duration);
} else {
setState(() {});
}
}
void _animateRemoved(int index, DocumentSnapshot old) {
final animatedListState = _animatedListKey.currentState;
if (animatedListState != null) {
animatedListState.removeItem(
index,
(BuildContext context, Animation<double> animation) {
return widget.itemBuilder(context, index, old, animation);
},
duration: widget.duration,
);
} else {
setState(() {});
}
}
#override
Widget build(BuildContext context) {
if (_dataListen.length == 0 &&
_dataPaged.length == 0 &&
!widget.initialAnimate) {
return SizedBox();
}
return AnimatedList(
key: _animatedListKey,
controller: controller,
scrollDirection: widget.scrollDirection,
reverse: widget.reverse,
primary: widget.primary,
physics: widget.physics,
shrinkWrap: widget.shrinkWrap,
padding: widget.padding ?? MediaQuery.of(context).padding,
initialItemCount: _dataListen.length + _dataPaged.length,
itemBuilder: (
BuildContext context,
int index,
Animation<double> animation,
) {
if (index < _dataListen.length) {
return widget.itemBuilder(
context,
index,
_dataListen[index],
animation,
);
} else {
final pagedIndex = index - _dataListen.length;
if (pagedIndex < _dataPaged.length) {
return widget.itemBuilder(
context, index, _dataPaged[pagedIndex], animation);
} else {
return widget.loaderBuilder(
context,
pagedIndex,
AlwaysStoppedAnimation<double>(1.0),
);
}
}
},
);
}
}
you can check this github project by simplesoft-duongdt3;
TLDR this is how to go about it
StreamController<List<DocumentSnapshot>> _streamController =
StreamController<List<DocumentSnapshot>>();
List<DocumentSnapshot> _products = [];
bool _isRequesting = false;
bool _isFinish = false;
void onChangeData(List<DocumentChange> documentChanges) {
var isChange = false;
documentChanges.forEach((productChange) {
print(
"productChange ${productChange.type.toString()} ${productChange.newIndex} ${productChange.oldIndex} ${productChange.document}");
if (productChange.type == DocumentChangeType.removed) {
_products.removeWhere((product) {
return productChange.document.documentID == product.documentID;
});
isChange = true;
} else {
if (productChange.type == DocumentChangeType.modified) {
int indexWhere = _products.indexWhere((product) {
return productChange.document.documentID == product.documentID;
});
if (indexWhere >= 0) {
_products[indexWhere] = productChange.document;
}
isChange = true;
}
}
});
if(isChange) {
_streamController.add(_products);
}
}
#override
void initState() {
Firestore.instance
.collection('products')
.snapshots()
.listen((data) => onChangeData(data.documentChanges));
requestNextPage();
super.initState();
}
#override
void dispose() {
_streamController.close();
super.dispose();
}
#override
Widget build(BuildContext context) {
return NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification scrollInfo) {
if (scrollInfo.metrics.maxScrollExtent == scrollInfo.metrics.pixels) {
requestNextPage();
}
return true;
},
child: StreamBuilder<List<DocumentSnapshot>>(
stream: _streamController.stream,
builder: (BuildContext context,
AsyncSnapshot<List<DocumentSnapshot>> snapshot) {
if (snapshot.hasError) return new Text('Error: ${snapshot.error}');
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return new Text('Loading...');
default:
log("Items: " + snapshot.data.length.toString());
return //your grid here
ListView.separated(
separatorBuilder: (context, index) => Divider(
color: Colors.black,
),
itemCount: snapshot.data.length,
itemBuilder: (context, index) => Padding(
padding: const EdgeInsets.symmetric(vertical: 32),
child: new ListTile(
title: new Text(snapshot.data[index]['name']),
subtitle: new Text(snapshot.data[index]['description']),
),
),
);
}
},
));
}
void requestNextPage() async {
if (!_isRequesting && !_isFinish) {
QuerySnapshot querySnapshot;
_isRequesting = true;
if (_products.isEmpty) {
querySnapshot = await Firestore.instance
.collection('products')
.orderBy('index')
.limit(5)
.getDocuments();
} else {
querySnapshot = await Firestore.instance
.collection('products')
.orderBy('index')
.startAfterDocument(_products[_products.length - 1])
.limit(5)
.getDocuments();
}
if (querySnapshot != null) {
int oldSize = _products.length;
_products.addAll(querySnapshot.documents);
int newSize = _products.length;
if (oldSize != newSize) {
_streamController.add(_products);
} else {
_isFinish = true;
}
}
_isRequesting = false;
}
}