In ListView.builder Bloc event triggered only once - flutter

I m using Bloc for state management , I have my screen where I'm calling event in ListView.builder
loadSuccess: (state) {
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: state.questions.size,
itemBuilder: (
context,
index,
) {
// ignore: avoid_unnecessary_containers
debugPrint("this is index $index");
debugPrint(
"this is user id ${state.questions.get(index).userId.getorCrash()}");
context.read<UsersWatcherBloc>().add(
UsersWatcherEvent.watchAllUsers(
state.questions.get(index).userId.getorCrash(),
),
);
return Container(
color: Colors.white,
......)
But problem is that my event is triggered only one time and state changed one time but I want to change my event for each index :
Event.dart:
part of 'users_watcher_bloc.dart';
#freezed
abstract class UsersWatcherEvent with _$UsersWatcherEvent {
const factory UsersWatcherEvent.watchAllUsers(String uId) = _Started;
}
Bloc.dart:
#injectable
class UsersWatcherBloc extends Bloc<UsersWatcherEvent, UsersWatcherState> {
final IElearningRepository _iElearningRepository;
UsersWatcherBloc(this._iElearningRepository)
: super(const UsersWatcherState.initial());
#override
Stream<UsersWatcherState> mapEventToState(
UsersWatcherEvent event,
) async* {
yield* event.map(
watchAllUsers: (e) async* {
print("this is user id ${e.uId}");
yield const UsersWatcherState.loadInProgress();
yield* _iElearningRepository.watchAllUsers(e.uId.toString()).map(
(failureOrUsers) => failureOrUsers.fold(
(f) => UsersWatcherState.loadFailure(f),
(users) {
if (users.isEmpty) {
return const UsersWatcherState.empty();
}
return UsersWatcherState.loadSuccess(users);
},
),
);
},
);
}
}

After 2 days struggle I found solution of this question, I have to wrap my container with another BlocProvider and use dependency injection
loadSuccess: (state) {
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: state.questions.size,
itemBuilder: (
context,
index,
) {
// ignore: avoid_unnecessar_containers
return BlocProvider(
create: (context) => getIt<UsersWatcherBloc>()
..add(
UsersWatcherEvent.watchCurrentUser(
state.questions.get(index).userId.getorCrash(),
),
),
child: Container(
color: Colors.white,
margin: const EdgeInsets.only(bottom: 5),
padding: EdgeInsets.only(
left: leftPadding.w - 8.w,
right: rightpadding.w - 8.w,
bottom: bottomPadding.h,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [

Related

DraggableScrollableSheet with Navigator

I want to start discussion here about the DraggableScrollableSheet and how to possibly use it with nested Navigator.
The problem is simple and problematic at the same time.
Here is an example code:
First I have an Draggable Scrollable with custom Stateful widget MyCustomTabs
DraggableScrollableSheet(
builder: (context, scrollController) => Column(
children: [
Expanded(child: MyCustomTabs(
key: myCustomTabsKey,
scrollController: scrollController,)),
],
),
),
Secondly this is an implementation of MyCustomTabs
class MyCustomTabs extends StatefulWidget {
final ScrollController scrollController;
const MyCustomTabs({Key? key, required this.scrollController})
: super(key: key);
#override
State<MyCustomTabs> createState() => MyCustomTabsState();
}
class MyCustomTabsState extends State<MyCustomTabs> {
int _page = 1;
set page(int value){
setState((){
_page = value;
});
print("set new great sttea");
}
#override
Widget build(BuildContext context) {
return Navigator(
pages: [
if(_page == 1)
MaterialPage(
child: ListView.builder(
controller: widget.scrollController,
itemBuilder: (context, index) => Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
height: 200,
color: Color(0xFF41DACC),
),
),
)),
if(_page == 2)
MaterialPage(
child: ListView.builder(
//THIS WILL CREATE AN ERROR
controller: widget.scrollController,
itemBuilder: (context, index) => Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
height: 200,
color: Color(0xFFAB27AF),
),
),
)),
],
);
}
}
This setup will cause an error if we try to attach scroll controller to different ListView widgets.
So Now I will try to solve this problems with use of notifications and my custom Scroll controllers pair.
One of which will send notifications about a scroll and the second will receive notifications from child widgets and pass it to DraggableScrollable
Or maybe there is another solution out of the box.

flutter listview builder inside a listview builder

I don't have much experience with flutter.
I would like to use the language_tool library (https://pub.dev/packages/language_tool) for Dart and Flutter.
To show the data obtained from the tool() function, I created a FutureBuilder with a ListView.builder inside, which returns a Column.
I would like there to be 2 children inside the column:
1- a Text with mistake.issueDescription as text (for each "mistake")
2- another ListView that returns the elements of the List mistake.replacements for each "mistake"
Anyone know how I can fix it?
Below I put the code I created, which works fine until I put the Listview builder inside the first ListView builder.
import 'package:flutter/material.dart';
import 'package:language_tool/language_tool.dart';
void main() => runApp(mainApp());
class mainApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return const MaterialApp(
home: Chat(),
);
}
}
class Chat extends StatefulWidget {
const Chat({Key? key}) : super(key: key);
#override
_ChatState createState() => _ChatState();
}
class _ChatState extends State<Chat> {
String text = 'Henlo i am Gabriele';
Future<List<WritingMistake>> tool(String text) async {
var tool = LanguageTool();
var result = tool.check(text);
var correction = await result;
List<WritingMistake> mistakes = [];
for (var m in correction) {
WritingMistake mistake = WritingMistake(
message: m.message,
offset: m.offset,
length: m.length,
issueType: m.issueType,
issueDescription: m.issueDescription,
replacements: m.replacements,
);
mistakes.add(mistake);
}
print(mistakes.length);
print(mistakes);
return mistakes;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: [
Container(
color: Colors.red,
height: 150.0,
width: double.infinity,
child: Center(
child: Text(text, style: const TextStyle(fontSize: 20.0))),
),
FutureBuilder(
future: tool(text),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.data == null) {
return const Center(
child: Text('Loading...'),
);
} else {
return SizedBox(
height: 200.0,
child: ListView.builder(
itemCount: snapshot.data.length,
itemBuilder:
(BuildContext context, int mistakeIdIndex) {
return Column(
children: [
Text(snapshot
.data[mistakeIdIndex].issueDescription),
// this is where the problems begin
ListView.builder(
itemCount: snapshot.data[mistakeIdIndex]
.replacements.length,
scrollDirection: Axis.horizontal,
itemBuilder:
(BuildContext context, int index) {
return Text(snapshot.data[mistakeIdIndex]
.replacements[index]);
}),
],
);
}),
);
}
}),
],
),
),
);
}
}
I hope I was clear and that someone can help me.
Thank you :)
You cannot give a listview-builder as a child for a column try changing the Column widget to a ListView and set its shrinkWrap property to true.
ListView(
children: [
Container(
color: Colors.red,
height: 150.0,
width: double.infinity,
child: Center(
child: Text(text, style: const TextStyle(fontSize: 20.0))),
),
FutureBuilder(
future: tool(text),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.data == null) {
return const Center(
child: Text('Loading...'),
);
} else {
return SizedBox(
height: 200.0,
child: ListView.builder(
shrinkWrap:true,
itemCount: snapshot.data.length,
itemBuilder:
(BuildContext context, int mistakeIdIndex) {
return ListView(
shrinkWrap:true,
children: [
Text(snapshot
.data[mistakeIdIndex].issueDescription),
// this is where the problems begin
ListView.builder(
shrinkWrap:true,
itemCount: snapshot.data[mistakeIdIndex]
.replacements.length,
scrollDirection: Axis.horizontal,
itemBuilder:
(BuildContext context, int index) {
return Text(snapshot.data[mistakeIdIndex]
.replacements[index]);
}),
],
);
}),
);
}
}),
],
),
),

Passing data to another screen with Flutter Provider

I'm trying to pass the data to another screen using Provider, but it seems I'm always passing on the same data unless I sort the List and then pass the different data (meaning I'm probably switching the index by sorting the list so that is why it's passing different data now). In short, I call the API, populate the list, setting up the provider too for the next page, and on click I list out the the information from the previous screen, but the problem is I display the same item always unless I sort the list. Here is the code:
Calling the API and displaying the list:
var posts = <RideData>[];
var streamController = StreamController<List<RideData>>();
#override
void initState() {
_getRideStreamList();
super.initState();
}
_getRideStreamList() async {
await Future.delayed(Duration(seconds: 3));
var _vehicleStreamData = await APICalls.instance.getRides();
var provider = Provider.of<RideStore>(context, listen: false);
posts = await _vehicleStreamData
.map<RideData>((e) => RideData.fromJson(e))
.toList();
streamController.add(posts);
provider.setRideList(posts, notify: false);
}
bool isSwitched = true;
void toggleSwitch(bool value) {
if (isSwitched == false) {
posts.sort((k1, k2) => k1.rideId.compareTo(k2.rideId));
} else {
posts.sort((k1, k2) => k2.rideId.compareTo(k1.rideId));
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Column(
children: [
TextButton(
child: Text('sort ascending'),
onPressed: () {
setState(() {
toggleSwitch(isSwitched = !isSwitched);
});
}),
Container(
height: 1000,
child: StreamBuilder<List<RideData>>(
initialData: posts,
stream: streamController.stream,
builder: (context, snapshot) {
return ListView.builder(
shrinkWrap: true,
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
return Column(
children: [
Row(
children: [
Padding(
padding: const EdgeInsets.only(left: 15.0),
child: Text(
'Ride #${snapshot.data[index].rideId}',
),
),
FlatButton(
textColor: Colors.blue[700],
minWidth: 0,
child: Text('View'),
onPressed: () {
// here is where I pass the data to the RideInfo screen
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RideInfo(
rideId: snapshot
.data[index].rideId,
)));
},
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'${snapshot.data[index].pickupTime}',
),
Text(
'${snapshot.data[index].jobArrived}',
),
],
),
],
);
},
);
}),
),
],
),
),
),
);
}
After pressing the View button and passing the data to another screen (RideInfo):
class RideInfo extends StatefulWidget {
static const String id = 'ride_info_screen';
String rideId;
RideInfo({#required this.rideId});
#override
_RideInfoState createState() => _RideInfoState();
}
class _RideInfoState extends State<RideInfo> {
String rideID = '';
#override
void initState() {
super.initState();
setState(() {
rideID = widget.rideId;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
title: Text(
'Ride #$rideID',
),
),
body: SafeArea(
child: SingleChildScrollView(
child: Consumer<RideStore>(
builder: (context, rideStore, child) {
return Column(
children: [
ListView.builder(
shrinkWrap: true,
itemCount: 1,
itemBuilder: (context, index) {
RideData rides = rideStore.getRideByIndex(index);
return Column(
children: [
Expanded(
flex: 2,
child: Column(
children: [
Text(
"PICK UP",
),
// here I display the pickUpTime but it is always the same and I wanted to display the time based on the ID
Text(
'${rides.pickupTime}AM',
),
],
),
),
],
);
}),
],
);
},
),
),
),
);
}
}
The data (pickUpTime in this case) doesn't change when I press to see the View of a single item, but like I said, when I change the order of the list with the sort method, then I get the different data.
Here is the Provider model:
class RideStore extends ChangeNotifier {
List<RideData> _rideList = [];
List<RideData> get rideList => _rideList;
setRideList(List<RideData> list, {bool notify = true}) {
_rideList = list;
if (notify) notifyListeners();
}
RideData getRideByIndex(int index) => _rideList[index];
int get rideListLength => _rideList.length;
}
How do I display the correct information based on the ID from the List that I pressed and passed in the Ride Info screen so it doesn't give back always the same data? Thanks in advance for the help!
The offending code is in RideInfo:
ListView.builder(
shrinkWrap: true,
itemCount: 1,
itemBuilder: (context, index) {
RideData rides = rideStore.getRideByIndex(index);
The index is always 1, so you are always showing the first RideData. There are various options to fix it, e.g. pass the index, or even pass the RideData, to the RideInfo constructor:
class RideInfo extends StatefulWidget {
static const String id = 'ride_info_screen';
String rideId;
final int index;
RideInfo({#required this.rideId, #required this.index, Key key})
: super(key: key) {
and:
RideData rides = rideStore.getRideByIndex(widget.index);
I have some additional comments on the code. Firstly, the ListView is serving no purpose in RideInfo, so remove it.
Secondly, there is no need to construct the streamController and to use StreamBuilder in the parent form. Your list is available in the RideStore. So your parent form could have:
Widget build(BuildContext context) {
var data = Provider.of<RideStore>(context).rideList;
...
Container(
height: 1000,
child:
// StreamBuilder<List<RideData>>(
// initialData: posts,
// stream: streamController.stream,
// builder: (context, snapshot) {
// return
ListView.builder(
shrinkWrap: true,
itemCount: data.length,
I hope these comments help.
Edit:
It is simple to edit your code to use FutureBuilder. Firstly, make _getRideStreamList return the data it read:
_getRideStreamList() async {
...
return posts;
}
Remove the call to _getRideStreamList in initState and wrap the ListView in the FutureBuilder that invokes _getRideStreamList:
Container(
height: 1000,
child: FutureBuilder(
future: _getRideStreamList(),
builder: (ctx, snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
} else {
var data = snapshot.data;
return ListView.builder(
...
);
}
},
),
),
This displays the CircularProgressIndicator while waiting for the data.
Note that this is a quick hack - you do not want to read the data everytime that the widget rebuilds. So _getRideStreamList could check if the data has already been read and just return it rather than rereading.

How to load more items to a list when reach the bottom of results flutter

I have the code below which feed a list with 10 results from firebase. In this case it shows only the 10 items, now I wanna, when user gets the bottom of results, it loads more 10 items and add it to the list. I already have the scrollController and it works.. I receive the log "LOAD HERE" when I get the bottom of the results.
My doubt is how to add the new 10 items in the list?
scrollListener() async {
if (scrollController.position.maxScrollExtent == scrollController.offset) {
print('LOAD HERE');
}
}
#override
void initState() {
scrollController.addListener(scrollListener);
super.initState();
}
#override
void dispose() {
scrollController.removeListener(scrollListener);
super.dispose();
}
loadList(submenu ,callback, context, deviceSize){
return FutureBuilder(
future: ctrlLab.loadList(submenu, 10),
builder: (ctx, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
} else if (snapshot.error != null) {
print(snapshot.error);
return Center(child: Text('ERROR!'));
}else {
return GridView.builder(
padding: EdgeInsets.all(10.0),
controller: scrollController,
itemCount: snapshot.data.length,
itemBuilder: (ctx, i) {
Item item = snapshot.data[i];
if (i < snapshot.data.length) {
return Dismissible(
key: UniqueKey(),
direction: DismissDirection.endToStart,
background: Container(
padding: EdgeInsets.all(10.0),
color: Colors.grey[800],
child: Align(
alignment: AlignmentDirectional.centerEnd,
child: Icon(
Icons.delete,
color: Colors.white,
size: 40,
),
),
),
onDismissed: (DismissDirection direction) {
ctrl.onDismissed(callback, item);
},
child: GestureDetector(
child: Card(
elevation: 5.0,
child: Padding(
padding: EdgeInsets.all(10.0),
child: GridTile(
child: Hero(
tag: "${item}",
child: item.imageUrl == null
? setIconLab(item)
: CachedNetworkImage(
fit: BoxFit.cover,
imageUrl: setIconLab(item),
placeholder: (ctx, url) =>
Center(child: CircularProgressIndicator()),
errorWidget: (context, url, error) =>
Image.asset('assets/images/noPhoto.jpg',
fit: BoxFit.cover),
),
),
footer: Container(
padding: EdgeInsets.all(8.0),
color: Colors.white70,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
item.name
),
),
],
),
),
),
),
),
),
);
}
},
gridDelegate: SliverGridDelegateWithFixedCrossAxisCountAndLoading(
itemCount: snapshot.data.length + 1,
crossAxisCount: deviceSize.width < 600 ? 2 : 3,
childAspectRatio: 0.7,
crossAxisSpacing: 10.0,
mainAxisSpacing: 10.0,
),
);
}
},
);
}
Infinite Scrolling in ListView
I have achieved this case by using the local field instead of getting data from firebase. Hope it will give you some idea.
import 'package:flutter/material.dart';
class ListViewDemo extends StatefulWidget {
ListViewDemo({Key key}) : super(key: key);
#override
_ListViewDemoState createState() => _ListViewDemoState();
}
class _ListViewDemoState extends State<ListViewDemo> {
ScrollController controller;
int count = 15;
#override
void initState() {
super.initState();
controller = ScrollController()..addListener(handleScrolling);
}
void handleScrolling() {
if (controller.offset >= controller.position.maxScrollExtent) {
setState(() {
count += 10;
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('List view'),
),
body: ListView.builder(
controller: controller,
itemCount: count,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text('Item $index'),
);
},
),
);
}
#override
void dispose() {
controller.removeListener(handleScrolling);
super.dispose();
}
}
You have to add another 10 data to the crtLap.loadList(subMenu, 20) and call setState inside the scrollListener to rebuild the widget about the changes.
var data = crtLap.loadList(subMenu, 10);
scrollListener() async {
if (scrollController.position.maxScrollExtent == scrollController.offset) {
setState((){
data = crtLap.loadList(subMenu, 20);
});
}
}
and use this data field to the FutureBuilder directly,
loadList(submenu ,callback, context, deviceSize){
return FutureBuilder(
future: data,
builder: (ctx, snapshot) {
.....
...
..
}

Stream has already been listened to, error after change screens

I am applying the Bloc pattern in my application and I encountered problems when changing the screen by a Bottom Navigation.
I tried to make a Stream Broadcast, however after changing the screen the data disappears.
Here is my Home class, it represents the home screen.
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
final DepartmentBloc departmentBloc = BlocProvider.getBloc<DepartmentBloc>();
final PromotionProductBloc promotionProductBloc = BlocProvider.getBloc<PromotionProductBloc>();
#override
Widget build(BuildContext context) {
return SingleChildScrollView(
physics: BouncingScrollPhysics(),
child: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
CustomText(
text: "Promoções",
fontSize: 20.0,
padding: EdgeInsets.only(left: 12.0),
),
StreamBuilder(
stream: promotionProductBloc.outPromotionProducts,
builder: (context, snapshot) {
if (snapshot.hasData)
return Container(
height: 230.0,
child: ListView.builder(
padding: EdgeInsets.only(
left: 12.0, right: 12.0, bottom: 10.0),
itemBuilder: (context, index) {
return PromotionProductCard(snapshot.data[index]);
},
itemCount: snapshot.data.length,
scrollDirection: Axis.horizontal,
physics: BouncingScrollPhysics(),
),
);
else
return Container(
height: 200.0,
);
},
),
CustomText(
text: "Categorias",
fontSize: 20.0,
padding: EdgeInsets.only(left: 12.0, top: 20.0, bottom: 10.0),
),
StreamBuilder(
stream: departmentBloc.outDepartments,
builder: (context, snapshot) {
if (snapshot.hasData)
return Container(
height: 120.0,
child: ListView.builder(
padding: EdgeInsets.only(left: 12.0, right: 12.0),
itemBuilder: (context, index) {
return CategoryCard(snapshot.data[index]);
},
itemCount: snapshot.data.length,
scrollDirection: Axis.horizontal,
physics: BouncingScrollPhysics(),
),
);
else
return Container(
height: 100.0,
);
},
),
],
),
),
);
}
Here is the class that accesses the Service to get the data and opens an exit to the Stream.
class DepartmentBloc extends BlocBase {
DepartmentService departmentService;
List<Department> departments;
final StreamController<List<Department>> _departmentController = StreamController<List<Department>>();
Stream<List<Department>> get outDepartments => _departmentController.stream;
DepartmentBloc() {
departmentService = DepartmentService();
getAll();
}
void getAll() async {
departments = await departmentService.getAll();
_departmentController.sink.add(departments);
}
#override
void dispose() {
_departmentController.close();
super.dispose();
}
}
PromotionProductBloc is the same as DepartmentBloc, it only changes Service access.
I'm using a Bottom Navigation, after going to another screen and back appears the following error: Stream has already been listened to.
Try
final StreamController<List<Department>> _departmentController = StreamController.broadcast<List<Department>>();
Seems like you are using a single subscription stream controller. They can only be listened once. An advantage of them is that first listener will receive those event that were emitted before subscribing to it. Broadcast streams do not guarantee this behavior but they can be subscribed to multiple times.