Pagination for Firestore animated list flutter - 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;
}
}

Related

Using ScrollController to fetch pages of data doesn't update GridView with updated list

When the Artworks screen is first rendered, it makes an async call that fetches the first page of data and then displays it in a grid. In the initState() function, I also add a scroll listener that fetches the next available page of data when the user reaches the bottom of the screen. The fetching logic works just fine, but the GridView doesn't seem to be rebuilt with the newly enhanced list despite calling setState(). Any ideas on why that is?
Artworks screen:
class Artworks extends StatefulWidget {
const Artworks({
Key? key,
}) : super(key: key);
#override
State<StatefulWidget> createState() => _ArtworksState();
}
class _ArtworksState extends State<Artworks> {
AlbumCubit get _cubit => context.read<AlbumCubit>();
List<Photo> get artworks => _cubit.artworks;
final ArtworkRepository _artworkRepository = getIt<ArtworkRepository>();
final ScrollController _controller = ScrollController();
int _totalArtworkPages = 0;
int _currentPage = 0;
bool fetchedFirstPage = false;
#override
void initState() {
super.initState();
_controller.addListener(() async {
if (_controller.position.atEdge) {
final bool isTop = _controller.position.pixels == 0;
if (!isTop) {
// scrolled to the bottom, fetch new page if available
if (_currentPage < _totalArtworkPages - 1) {
await _artworkRepository.getSavedArtworks(page: ++_currentPage);
setState(() {});
}
}
}
});
initData();
}
Future<void> initData() async {
_totalArtworkPages =
await _artworkRepository.getSavedArtworks(page: _currentPage);
setState(() {
fetchedFirstPage = true;
});
}
#override
Widget build(BuildContext context) {
return (!fetchedFirstPage)
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const <Widget>[
CircularProgressIndicator(color: AppColors.red),
SizedBox(
height: 10,
),
Text(
'Loading artworks',
style: montserratMedium14,
)
],
),
)
: GridView.builder(
controller: _controller,
itemCount: _artworkRepository.savedArtworks.length,
padding: const EdgeInsets.symmetric(horizontal: 16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 4,
mainAxisSpacing: 4,
),
itemBuilder: (BuildContext context, int index) {
final Photo artwork = _artworkRepository.savedArtworks[index];
return ArtworkItem(artwork: artwork);
},
);
}
}
class ArtworkItem extends StatelessWidget {
const ArtworkItem({Key? key, required this.artwork}) : super(key: key);
final Photo artwork;
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () =>
NavigatorUtils.goToArtworkViewScreen(context, artworkId: artwork.id),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: AppColors.grey,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: CachedNetworkImage(
placeholder: (context, url) => const Center(
child: CircularProgressIndicator(
color: AppColors.red,
),
),
errorWidget: (context, url, dynamic error) =>
const Icon(Icons.error),
imageUrl: artwork.thumbnailPhoto ?? artwork.firebasePhoto,
fit: BoxFit.cover,
),
),
),
);
}
}
ArtworksRepositoryImpl:
class ArtworkRepositoryImpl implements ArtworkRepository {
ArtworkRepositoryImpl({required DioClient dioClient}) : _client = dioClient;
final DioClient _client;
final String baseUrl = getIt<AppRepository>().env == Env.staging //
? stagingUrl
: productionUrl;
final ArtistRepository artistRepository = getIt<ArtistRepository>();
#override
String? token;
#override
List<Photo> savedArtworks = [];
#override
Future<int> getSavedArtworks(
{int page = 0, int pageSize = 20, bool ascendent = true}) async {
try {
final ApiResponse response = await _client.httpCall(
baseUrl: baseUrl,
path: '/mobile/artwork/list',
httpMethod: HttpMethod.GET,
headers: <String, dynamic>{'x-auth-token': token},
queryParameters: <String, dynamic>{
'p': page,
'size': pageSize,
'asc': ascendent
},
);
final Map<String, dynamic> data = response.data;
final List<dynamic> artworksJson = data['results'] as List<dynamic>;
savedArtworks.addAll(
artworksJson.map((dynamic json) => Photo.fromJson(json)).toList(),
);
savedArtworks = savedArtworks.toSet().toList(); // remove duplicates
return data['totalPages'] as int;
} catch (e) {
rethrow;
}
}
}
Maybe it work,if you have not try you can create a function and replace wit in your listener like :
_controller.addListener(_onScroll);
then create _onScroll
Future _onScroll() async{
if (_controller.position.atEdge) {
final bool isTop = _controller.position.pixels == 0;
if (!isTop) {
// scrolled to the bottom, fetch new page if available
if (_currentPage < _totalArtworkPages - 1) {
await _artworkRepository.getSavedArtworks(page: ++_currentPage);
setState(() {});
}
}
}
}
and check the link below
https://www.youtube.com/watch?v=eENDlIgadr4

Flutter : scrollController.isAttached is always false

How can I scroll to a special widget in a ListView? For example, I want to automatically scroll to some container in ListView if I press a certain button on a previous screen. I will pass to the next screen an Id (from id I will know the index) and when I navigate to the next screen I want to automatically scroll to this widget.
the code in main screen : Navigator.push(context, MaterialPageRoute(builder: (_) => CreatedEstatesScreen(estateId: id)));
the code in the next screen :
class RecentEstateOrdersScreen extends StatefulWidget {
static const String id = "RecentEstateOrdersScreen";
String? estateId;
RecentEstateOrdersScreen({Key? key, this.estateId}) : super(key: key);
#override
_RecentEstateOrdersScreenState createState() =>
_RecentEstateOrdersScreenState();
}
class _RecentEstateOrdersScreenState extends State<RecentEstateOrdersScreen> {
late RecentEstatesOrdersBloc _recentEstatesOrdersBloc;
late ItemScrollController scrollController;
late ItemPositionsListener itemPositionsListener;
String? userToken;
List<EstateOrder> orders = [];
#override
void initState() {
super.initState();
_recentEstatesOrdersBloc = RecentEstatesOrdersBloc(EstateOrderRepository());
_onRefresh();
User? user = BlocProvider.of<UserLoginBloc>(context).user;
if (user != null && user.token != null) {
userToken = user.token;
}
scrollController = ItemScrollController();
itemPositionsListener = ItemPositionsListener.create();
}
_onRefresh() {
if (BlocProvider.of<UserLoginBloc>(context).user!.token != null) {
_recentEstatesOrdersBloc.add(
RecentEstatesOrdersFetchStarted(
token: BlocProvider.of<UserLoginBloc>(context).user!.token!),
);
}
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text(
AppLocalizations.of(context)!.recent_created_orders,
),
),
body: BlocConsumer<RecentEstatesOrdersBloc, RecentEstatesOrdersState>(
bloc: _recentEstatesOrdersBloc,
listener: (context, recentOrdersState) async {
if (recentOrdersState is RecentEstatesOrdersFetchError) {
var error = recentOrdersState.isConnectionError
? AppLocalizations.of(context)!.no_internet_connection
: recentOrdersState.error;
await showWonderfulAlertDialog(
context, AppLocalizations.of(context)!.error, error);
}
},
builder: (BuildContext context, recentOrdersState) {
if (recentOrdersState is RecentEstatesOrdersFetchProgress) {
return const ClientsOrdersShimmer();
}
if (recentOrdersState is! RecentEstatesOrdersFetchComplete) {
return Container();
}
orders = recentOrdersState.estateOrders;
if (orders.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SvgPicture.asset(
documentOutlineIconPath,
width: 0.5.sw,
height: 0.5.sw,
color: Theme.of(context)
.colorScheme
.onBackground
.withOpacity(0.42),
),
48.verticalSpace,
Text(
AppLocalizations.of(context)!.have_not_recent_orders,
style: Theme.of(context).textTheme.headline4,
),
],
),
);
}
if (widget.estateId != null) {
SchedulerBinding.instance!.addPostFrameCallback((_) {
jumpToOrder(orders);
});
}
return RefreshIndicator(
color: Theme.of(context).colorScheme.primary,
onRefresh: () async {
_onRefresh();
},
child: ListView.builder(
itemCount: orders.length,
itemBuilder: (_, index) {
return EstateOrderCard(
estateOrder: orders.elementAt(index),
);
}),
);
},
),
),
);
}
jumpToOrder(List<EstateOrder> orders) {
int index = getIndexFromId(orders);
if (index != -1) {
if (scrollController.isAttached) {
scrollController.scrollTo(
index: index,
duration: const Duration(seconds: 2),
curve: Curves.easeInOutCubic);
}
}
}
getIndexFromId(List<EstateOrder> orders) {
for (int i = 0; i < orders.length; i++) {
if (orders.elementAt(i).id == int.parse(widget.estateId!)) {
return i;
}
}
return -1;
}
}```
If you are using the library then you have to use ScrollablePositionedList.builder, not the normal ListView.builder.

How to Set Textfield in flutter to accept only certain values specified from a list or variable in Flutter

I am working on a web app where users can post stuffs and make them more accessible by associating the posts with tags. so my idea is similar to stackoverflow's way of giving tags to posts, I am creating a Textfield with which will accept only few tags(string values) which I will create from a list and users can put them in their post. But I aint getting how to implement this as textfield has only few keyboardtypes... and I what I want to achieve is if I entered a value from a that list then it should act like a chip text(tag).
or Is there any other way to do this,
your help is appreciated,
thank you
Yes, there is. you can use the flutter_tagging package on the PUB
It has supports for Web
The gif below explains what you want to achieve
You can find an implementation of a Chip Input Field type widget here:
Latest: https://gist.github.com/slightfoot/c6c0f1f1baca326a389a9aec47886ad6
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
// See: https://twitter.com/shakil807/status/1042127387515858949
// https://github.com/pchmn/MaterialChipsInput/tree/master/library/src/main/java/com/pchmn/materialchips
// https://github.com/BelooS/ChipsLayoutManager
void main() => runApp(ChipsDemoApp());
class ChipsDemoApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primaryColor: Colors.indigo,
accentColor: Colors.pink,
),
home: DemoScreen(),
);
}
}
class DemoScreen extends StatefulWidget {
#override
_DemoScreenState createState() => _DemoScreenState();
}
class _DemoScreenState extends State<DemoScreen> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Material Chips Input'),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
decoration: const InputDecoration(hintText: 'normal'),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: ChipsInput<AppProfile>(
decoration: InputDecoration(prefixIcon: Icon(Icons.search), hintText: 'Profile search'),
findSuggestions: _findSuggestions,
onChanged: _onChanged,
chipBuilder: (BuildContext context, ChipsInputState<AppProfile> state, AppProfile profile) {
return InputChip(
key: ObjectKey(profile),
label: Text(profile.name),
avatar: CircleAvatar(
backgroundImage: NetworkImage(profile.imageUrl),
),
onDeleted: () => state.deleteChip(profile),
onSelected: (_) => _onChipTapped(profile),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
);
},
suggestionBuilder: (BuildContext context, ChipsInputState<AppProfile> state, AppProfile profile) {
return ListTile(
key: ObjectKey(profile),
leading: CircleAvatar(
backgroundImage: NetworkImage(profile.imageUrl),
),
title: Text(profile.name),
subtitle: Text(profile.email),
onTap: () => state.selectSuggestion(profile),
);
},
),
),
),
],
),
);
}
void _onChipTapped(AppProfile profile) {
print('$profile');
}
void _onChanged(List<AppProfile> data) {
print('onChanged $data');
}
Future<List<AppProfile>> _findSuggestions(String query) async {
if (query.length != 0) {
return mockResults.where((profile) {
return profile.name.contains(query) || profile.email.contains(query);
}).toList(growable: false);
} else {
return const <AppProfile>[];
}
}
}
// -------------------------------------------------
const mockResults = <AppProfile>[
AppProfile('Stock Man', 'stock#man.com', 'https://d2gg9evh47fn9z.cloudfront.net/800px_COLOURBOX4057996.jpg'),
AppProfile('Paul', 'paul#google.com', 'https://mbtskoudsalg.com/images/person-stock-image-png.png'),
AppProfile('Fred', 'fred#google.com',
'https://media.istockphoto.com/photos/feeling-great-about-my-corporate-choices-picture-id507296326'),
AppProfile('Bera', 'bera#flutter.io',
'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'),
AppProfile('John', 'john#flutter.io',
'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'),
AppProfile('Thomas', 'thomas#flutter.io',
'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'),
AppProfile('Norbert', 'norbert#flutter.io',
'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'),
AppProfile('Marina', 'marina#flutter.io',
'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'),
];
class AppProfile {
final String name;
final String email;
final String imageUrl;
const AppProfile(this.name, this.email, this.imageUrl);
#override
bool operator ==(Object other) =>
identical(this, other) || other is AppProfile && runtimeType == other.runtimeType && name == other.name;
#override
int get hashCode => name.hashCode;
#override
String toString() {
return 'Profile{$name}';
}
}
// -------------------------------------------------
typedef ChipsInputSuggestions<T> = Future<List<T>> Function(String query);
typedef ChipSelected<T> = void Function(T data, bool selected);
typedef ChipsBuilder<T> = Widget Function(BuildContext context, ChipsInputState<T> state, T data);
class ChipsInput<T> extends StatefulWidget {
const ChipsInput({
Key key,
this.decoration = const InputDecoration(),
#required this.chipBuilder,
#required this.suggestionBuilder,
#required this.findSuggestions,
#required this.onChanged,
this.onChipTapped,
}) : super(key: key);
final InputDecoration decoration;
final ChipsInputSuggestions findSuggestions;
final ValueChanged<List<T>> onChanged;
final ValueChanged<T> onChipTapped;
final ChipsBuilder<T> chipBuilder;
final ChipsBuilder<T> suggestionBuilder;
#override
ChipsInputState<T> createState() => ChipsInputState<T>();
}
class ChipsInputState<T> extends State<ChipsInput<T>> implements TextInputClient {
static const kObjectReplacementChar = 0xFFFC;
Set<T> _chips = Set<T>();
List<T> _suggestions;
int _searchId = 0;
FocusNode _focusNode;
TextEditingValue _value = TextEditingValue();
TextInputConnection _connection;
String get text => String.fromCharCodes(
_value.text.codeUnits.where((ch) => ch != kObjectReplacementChar),
);
bool get _hasInputConnection => _connection != null && _connection.attached;
void requestKeyboard() {
if (_focusNode.hasFocus) {
_openInputConnection();
} else {
FocusScope.of(context).requestFocus(_focusNode);
}
}
void selectSuggestion(T data) {
setState(() {
_chips.add(data);
_updateTextInputState();
_suggestions = null;
});
widget.onChanged(_chips.toList(growable: false));
}
void deleteChip(T data) {
setState(() {
_chips.remove(data);
_updateTextInputState();
});
widget.onChanged(_chips.toList(growable: false));
}
#override
void initState() {
super.initState();
_focusNode = FocusNode();
_focusNode.addListener(_onFocusChanged);
}
void _onFocusChanged() {
if (_focusNode.hasFocus) {
_openInputConnection();
} else {
_closeInputConnectionIfNeeded();
}
setState(() {
// rebuild so that _TextCursor is hidden.
});
}
#override
void dispose() {
_focusNode?.dispose();
_closeInputConnectionIfNeeded();
super.dispose();
}
void _openInputConnection() {
if (!_hasInputConnection) {
_connection = TextInput.attach(this, TextInputConfiguration());
_connection.setEditingState(_value);
}
_connection.show();
}
void _closeInputConnectionIfNeeded() {
if (_hasInputConnection) {
_connection.close();
_connection = null;
}
}
#override
Widget build(BuildContext context) {
var chipsChildren = _chips
.map<Widget>(
(data) => widget.chipBuilder(context, this, data),
)
.toList();
final theme = Theme.of(context);
chipsChildren.add(
Container(
height: 32.0,
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text(
text,
style: theme.textTheme.subhead.copyWith(
height: 1.5,
),
),
_TextCaret(
resumed: _focusNode.hasFocus,
),
],
),
),
);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
//mainAxisSize: MainAxisSize.min,
children: <Widget>[
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: requestKeyboard,
child: InputDecorator(
decoration: widget.decoration,
isFocused: _focusNode.hasFocus,
isEmpty: _value.text.length == 0,
child: Wrap(
children: chipsChildren,
spacing: 4.0,
runSpacing: 4.0,
),
),
),
Expanded(
child: ListView.builder(
itemCount: _suggestions?.length ?? 0,
itemBuilder: (BuildContext context, int index) {
return widget.suggestionBuilder(context, this, _suggestions[index]);
},
),
),
],
);
}
#override
void updateEditingValue(TextEditingValue value) {
final oldCount = _countReplacements(_value);
final newCount = _countReplacements(value);
setState(() {
if (newCount < oldCount) {
_chips = Set.from(_chips.take(newCount));
}
_value = value;
});
_onSearchChanged(text);
}
int _countReplacements(TextEditingValue value) {
return value.text.codeUnits.where((ch) => ch == kObjectReplacementChar).length;
}
#override
void performAction(TextInputAction action) {
_focusNode.unfocus();
}
void _updateTextInputState() {
final text = String.fromCharCodes(_chips.map((_) => kObjectReplacementChar));
_value = TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: text.length),
composing: TextRange(start: 0, end: text.length),
);
_connection.setEditingState(_value);
}
void _onSearchChanged(String value) async {
final localId = ++_searchId;
final results = await widget.findSuggestions(value);
if (_searchId == localId && mounted) {
setState(() => _suggestions = results.where((profile) => !_chips.contains(profile)).toList(growable: false));
}
}
}
class _TextCaret extends StatefulWidget {
const _TextCaret({
Key key,
this.duration = const Duration(milliseconds: 500),
this.resumed = false,
}) : super(key: key);
final Duration duration;
final bool resumed;
#override
_TextCursorState createState() => _TextCursorState();
}
class _TextCursorState extends State<_TextCaret> with SingleTickerProviderStateMixin {
bool _displayed = false;
Timer _timer;
#override
void initState() {
super.initState();
_timer = Timer.periodic(widget.duration, _onTimer);
}
void _onTimer(Timer timer) {
setState(() => _displayed = !_displayed);
}
#override
void dispose() {
_timer.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return FractionallySizedBox(
heightFactor: 0.7,
child: Opacity(
opacity: _displayed && widget.resumed ? 1.0 : 0.0,
child: Container(
width: 2.0,
color: theme.primaryColor,
),
),
);
}
}

Flutter BLoC - state doesn't triggers widget rebuild

the app is simple categories/products display , everything works fine except select a product from a category products and swip back to the products widget , the state changes and it's neither one of the states i created and just shows a loading indicator ( ProductsWrapper default return from state).
so here is the code :
ProductBloc :
class ProductBloc extends Bloc<ProductEvent, ProductState> {
final ProductRepository productRepository;
ProductBloc({required this.productRepository}) : super(ProductsEmpty());
#override
Stream<Transition<ProductEvent, ProductState>> transformEvents(
Stream<ProductEvent> events,
TransitionFunction<ProductEvent, ProductState> transitionFn) {
return super.transformEvents(
events.debounceTime(const Duration(microseconds: 500)), transitionFn);
}
#override
Stream<ProductState> mapEventToState(ProductEvent event) async* {
if (event is FetchProducts) {
yield* _mapFetchProductsToState(event);
} else if (event is RefreshProducts) {
yield* _mapRefreshProductsToState(event);
} else if (event is FetchProduct) {
yield* _mapFetchProductToState(event);
} else if (event is RefreshProduct) {
yield* _mapRefreshProductToState(event);
}
}
Stream<ProductState> _mapFetchProductsToState(FetchProducts event) async* {
try {
final products =
(await productRepository.getCategoryProducts(event.categoryId));
yield ProductsLoaded(products: products.products!);
} catch (_) {
yield state;
}
}
Stream<ProductState> _mapRefreshProductsToState(
RefreshProducts event) async* {
try {
final products =
await productRepository.getCategoryProducts(event.categoryId);
yield ProductsLoaded(products: products.products!);
return;
} catch (_) {
yield state;
}
}
Stream<ProductState> _mapFetchProductToState(FetchProduct event) async* {
try {
final product =
(await productRepository.getProductDetails(event.productId));
yield ProductLoaded(product: product);
} catch (e) {
yield state;
}
}
Stream<ProductState> _mapRefreshProductToState(RefreshProduct event) async* {
try {
final product =
await productRepository.getProductDetails(event.productId);
yield ProductLoaded(product: product);
return;
} catch (_) {
yield state;
}
}
}
states :
abstract class ProductState extends Equatable {
const ProductState();
#override
List<Object?> get props => [];
}
class ProductsEmpty extends ProductState {}
class ProductEmpty extends ProductState {}
class ProductLoading extends ProductState {}
class ProductsLoading extends ProductState {}
class ProductLoaded extends ProductState {
final Product product;
const ProductLoaded({required this.product});
ProductLoaded copyWith({required Product product}) {
return ProductLoaded(product: product);
}
#override
List<Object?> get props => [product];
#override
String toString() => 'ProductLoaded { product: ${product.name}}';
}
class ProductsLoaded extends ProductState {
final List<Product> products;
const ProductsLoaded({required this.products});
ProductsLoaded copyWith({required List<Product> products}) {
return ProductsLoaded(products: products);
}
#override
List<Object?> get props => [products];
#override
String toString() => 'ProductLoaded { products: ${products.length}}';
}
class ProductError extends ProductState {}
ProductRepository ( ProductApiService is just the api and it's working fine ) :
class ProductRepository {
final ProductApiService productApiService;
ProductRepository({ProductApiService? productApiService})
: productApiService = productApiService ?? ProductApiService();
Future<Products> getCategoryProducts(int? categoryId) async {
return productApiService.fetchCategoryProducts(categoryId);
}
Future<Product> getProductDetails(int? productId) async {
return productApiService.fetchProductDetails(productId);
}
}
ProductsWrapper :
final int? categoryId;
const ProductsWrapper({Key? key, required this.categoryId}) : super(key: key);
#override
_ProductsWrapperState createState() => _ProductsWrapperState();
}
class _ProductsWrapperState extends State<ProductsWrapper> {
final _scrollController = ScrollController();
final _scrollThreshold = 200;
Completer _productsRefreshCompleter = new Completer();
List<Product> products = [];
GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
void _onScroll() {
final maxScroll = _scrollController.position.maxScrollExtent;
final currentScroll = _scrollController.position.pixels;
if (maxScroll - currentScroll <= _scrollThreshold) {
context
.read<ProductBloc>()
.add(FetchProducts(categoryId: widget.categoryId!));
}
}
#override
void initState() {
super.initState();
context
.read<ProductBloc>()
.add(FetchProducts(categoryId: widget.categoryId!));
_scrollController.addListener(_onScroll);
_productsRefreshCompleter = Completer();
}
#override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
final double itemHeight = 260;
final double itemWidth = size.width / 2;
return Scaffold(
key: _scaffoldKey,
body: BlocListener<ProductBloc, ProductState>(
listener: (context, state) {
if (state is ProductsLoaded) {
products = state.products;
_productsRefreshCompleter.complete();
}
},
child: Container(
margin: EdgeInsets.all(8.0),
child: BlocBuilder<ProductBloc, ProductState>(
builder: (context, state) {
if (state is ProductsLoading) {
print('a7a');
return Center(
child: LoadingIndicator(),
);
}
if (state is ProductsLoaded) {
products = state.products;
if (state.products.isEmpty) {
return Center(
child: Text("No Products Found in this category"),
);
}
return Scaffold(
body: SafeArea(
child: Container(
child: GridView.builder(
itemCount: products.length,
scrollDirection: Axis.vertical,
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio:
(itemWidth / itemHeight)),
itemBuilder: (context, index) => Card(
elevation: 0,
child: InkWell(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) =>
ProductDetailScreen(
productId:
products[index]
.id)));
},
child: Container(
child: Column(
mainAxisAlignment:
MainAxisAlignment.start,
crossAxisAlignment:
CrossAxisAlignment.center,
children: [
ClipRRect(
child: Image.network(
products[index]
.image!
.image
.toString(),
height: 150,
fit: BoxFit.fitWidth,
),
),
Padding(
padding: EdgeInsets.all(8.0),
child: Text(
products[index].name.toString(),
style: TextStyle(
color: Colors.black,
fontWeight:
FontWeight.bold),
),
),
Row(
mainAxisAlignment:
MainAxisAlignment
.spaceBetween,
children: [
Padding(
padding: EdgeInsets.all(12.0),
child: Text(
'\$${products[index].price.toString()}'),
),
Padding(
padding: EdgeInsets.only(
right: 8.0),
child: CircleAvatar(
backgroundColor:
Theme.of(context)
.primaryColor,
radius: 10,
child: IconButton(
padding: EdgeInsets.zero,
icon: Icon(
Icons.add,
size: 20,
),
color: Colors.white,
onPressed: () {},
),
),
)
],
)
],
),
),
),
)),
),
),
);
}
return Center(
child: LoadingIndicator(strokeWidth: 5.0,),
);
}))));
}
}
ProductDetailScreen :
class ProductDetailScreen extends StatefulWidget {
final int? productId;
const ProductDetailScreen({Key? key, required this.productId})
: super(key: key);
#override
_ProductDetailScreenState createState() => _ProductDetailScreenState();
}
class _ProductDetailScreenState extends State<ProductDetailScreen> {
Completer _productRefreshCompleter = new Completer();
Product product = new Product();
GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
#override
void initState() {
super.initState();
context.read<ProductBloc>().add(FetchProduct(productId: widget.productId));
}
#override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
body: BlocListener<ProductBloc, ProductState>(
listener: (context, state) {
if (state is ProductLoaded) {
product = state.product;
_productRefreshCompleter.complete();
_productRefreshCompleter = Completer();
}
},
child: Container(
child: BlocBuilder<ProductBloc, ProductState>(
builder: (context, state) {
if (state is ProductLoading) {
return Center(
child: LoadingIndicator(),
);
}
if (state is ProductLoaded) {
return Scaffold(
body: SafeArea(
child: Container(
child: Text(product.name.toString()),
),
),
);
}
return Center(
child: LoadingIndicator(
strokeWidth: 5.0,
),
);
},
),
),
),
);
}
}
any help is appreciated .
thanks for taking time reading this , have a nice day and stay safe.
The problem is that you are using one bloc to do 2 things. The products list is an entity, the single detail is another entity. And you need to use the properties of the states as a result inside blocBuilders.
Plus, you don't need any listener and completer. The bloc pattern refreshes all when state changes.
I have created a repo with a working solution.
https://github.com/eugenioamato/categoryproducts

Flutter pagination loading the same data as in page one when scrolling

I'm building a list of news from an api that has next page results as in the image attached.
The api has only two pages with 10 list items each page.
Data is being passed to the widget. My problem is that when I scroll down the view, it loads the same 10 list items from page one.
This is the api I'm using enter link description here
Rest API
//newsModal.dart
class NewsNote {
String banner_image;
String title;
String text;
String sport;
NewsNote(this.banner_image, this.title, this.text, this.sport);
NewsNote.fromJson(Map<String, dynamic> json) {
banner_image = json['banner_image'];
title = json['title'];
text = json['text'];
sport = json['sport'];
}
}
//page news
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:jabboltapp/models/newsModal.dart';
class JabNews extends StatefulWidget {
#override
_JabNewsState createState() => _JabNewsState();
}
class _JabNewsState extends State<JabNews> {
ScrollController _scrollController = ScrollController();
bool isLoading = false;
String url = "https://jabbolt.com/api/news";
List<NewsNote> _newsNotes = List<NewsNote>();
Future<List<NewsNote>> fetchNewsNotes() async {
if (!isLoading) {
setState(() {
isLoading = true;
});
var response = await http.get(url);
var newsNotes = List<NewsNote>();
if (response.statusCode == 200) {
url = jsonDecode(response.body)['next'];
var newsNotesJson = json.decode(response.body)["results"];
for (var newsNoteJson in newsNotesJson) {
newsNotes.add(NewsNote.fromJson(newsNoteJson));
}
setState(() {
isLoading = false;
_newsNotes.addAll(newsNotes);
});
} else {
setState(() {
isLoading = false;
});
}
return newsNotes;
}
}
#override
void initState() {
fetchNewsNotes().then((value) {
setState(() {
_newsNotes.addAll(value);
});
});
this.fetchNewsNotes();
super.initState();
_scrollController.addListener(() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
fetchNewsNotes();
}
});
}
#override
void dispose() {
_scrollController.dispose();
super.dispose();
}
Widget _buildProgressIndicator() {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Opacity(
opacity: isLoading ? 1.0 : 00,
child: CircularProgressIndicator(),
),
),
);
}
Widget _buildList() {
return ListView.builder(
itemBuilder: (BuildContext context, int index) {
if (index == _newsNotes.length) {
return _buildProgressIndicator();
} else {
return Padding(
padding: EdgeInsets.all(8.0),
child: Card(
child: ListTile(
title: Text((_newsNotes[index].title)),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailPage(_newsNotes[index])));
},
),
),
);
}
},
controller: _scrollController,
itemCount: _newsNotes.length,
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: dGrey,
appBar: AppBar(
title: Text(
"News",
style: TextStyle(
color: textGrey,
fontFamily: 'bison',
fontSize: 32.0,
letterSpacing: 1.2,
),
),
backgroundColor: Colors.transparent,
elevation: 0,
),
body: Container(
child: _buildList(),
),
);
}
}
You need to add the page number concatenation in the URL
https://jabbolt.com/api/news?page=2