Flutter: listen data from Bloc state - flutter

I have tabs in my app:
news
promotions
shop
Also, I have blocs for fetching data for each tab (one bloc per tab). I want show tab only if I get a not empty list of data from bloc. How can I get access to this?
I push events while initState():
void _fetchData() {
context
..read<FeedBloc>().add(const FeedEvent.fetch(isFirstFetch: true))
..read<PromotionsBloc>().add(const PromotionsEvent.fetch(isFirstFetch: true))
..read<ShopBloc>().add(const ShopEvent.fetch(isFirstFetch: true));
}

Use BlocBuilder to check if data is not empty. You can try something like this:
BlocBuilder<FeedBloc, FeedBlocState>(
builder: (context, state) {
if(state.news == null || state.news!.isEmpty) return const SizedBox.shrink();
return NewsTab();
}

Related

Flutter BlocProvider consumption

I'm implementing a BLoC pattern for state management in my Fluter application. As I'm new in Flutter and BLoC particularly I'm evolving its usage gradually.
For new I use BLoC to communicate between two pages. One page sends an asset to the BLoC and navigates to details page. The details page uses StreamBuilder to read from the BLoC and build page with according data:
AppWidget:
Widget build(BuildContext context) => MultiProvider(
providers: [
BlocProvider(create: (context) => AssetBloc())
...
Requesting page
_onAssetMenuAction(BuildContext context, AssetMenu value, Asset asset) {
switch (value) {
case AssetMenu.validate:
var bloc = BlocProvider.of<AssetBloc>(context);
bloc.validate(asset);
Navigator.push(context,
MaterialPageRoute(builder: (context) => ValidateAssetPage()));
break;
}
Validation page
Widget build(BuildContext context) {
var bloc = BlocProvider.of<AssetBloc>(context);
Logger.root.info("Building validation page");
return StreamBuilder<AssetValidation>(
stream: bloc.outValidation,
builder: (context, snapshot) => snapshot.hasData
? QrImage.withQr(qr: snapshot.data!.qr)
: Text("No QR"));
}
BLoC
class AssetBloc extends BlocBase {
//
// Stream to handle the validation request outcome
//
StreamController<AssetValidation> _validationController =
StreamController<AssetValidation>.broadcast();
StreamSink<AssetValidation> get _inValidation => _validationController.sink;
Stream<AssetValidation> get outValidation => _validationController.stream;
//
// Stream to handle the validation request
//
StreamController<Asset> _validateController = StreamController<Asset>();
void Function(Asset) get validate => _validateController.sink.add;
//
// Constructor
//
AssetBloc([state]) : super(state) {
_validateController.stream.listen(_handleLogic);
}
void _handleLogic(Asset asset) {
_inValidation.add(AssetValidation.create(asset));
Logger.root.finest("AssetValidation instance is sent to stream");
}
void dispose() {
_validateController.close();
_validationController.close();
}
}
The problem I have is I'm getting "No QR". According to logs I see following sequence of actions:
new AssetValidation.create(): Validating asset Instance of 'Asset'
AssetBloc._handleLogic(): AssetValidation instance is sent to stream
ValidateAssetPage.build(): Building validation page
So at the moment of validation page building the validation result data should be in the stream but it seems they are not.
Unit tests of AssetBloc work as expected. So I suspect it should be related to StreamBuilder in validation page.
The StreamBuilder just shows you the last value of the stream whether the StreamBuilder was present on the current deployed widget when the stream was updated. So, if you add a new value to the stream, but the StreamBuilder is not on the current deployed widget, and, after that, you deploy the widget with the StreamBuilder, it's very likely that it won't show the updated data (in fact it shows empty data). I know, it's weird, i have the same problem when i like to use streams in that way. So, instead, i recommend you to use ValueListenable on the bloc and ValueListenableBuilder on the widget. It's very useful for that cases.
Another thing to point out is that if you're going to use just streams for the state management, it's better to use another state manager type such as provider or singleton. The reason is that, the right way to use bloc (the way you take advantage of the power of bloc) is using just the method add() for the events and logic, and using the established bloc State classes to show and update the data with the BlocBuilder on the widget.

how can I call async function after BlocBuilder state is success?

I have tasks list from flutter_downloader but that list is not enough to show in list. I need to show other information in list view as well as download information.
I already got the download tasks in initial state but I need to wait to get another list from bloc. after DownloadedSongListLoaded, I want to call _combineList(favouriteSongs); But I only want to return the Container after _combineList(favouriteSongs) finished.
So, how can I call async function in widget in BlocBuilder or other way around.
child: BlocBuilder<FavouriteSongBloc, FavouriteSongState>(
builder: (context, state) {
if (state is FavouriteSongError) {
return SomethingWentWrongScreen(error: state.error);
} else if (state is DownloadedSongListLoaded) {
favouriteSongs = state.favouriteSongs;
await _combineList(favouriteSongs); <== here, I want to manipulate the favouriteSongs list before binding the below Container widget.
return const Container() //ListView builder will be here
}
return const CircularProgressIndicatorWidget();
},
)

Flutter : same BlocBuilder on two consecutive pages

I'm working on an application and we decided to use the BLoC pattern.
I am facing a recurrent problem in my application.
Indeed, I created a bloc called CatalogBloc.
On my first page, there is a widget that uses the following BlocBuilder:
...
BlocBuilder<CatalogBloc, CatalogState>(
buildWhen: (previous, current) {
return current is CatalogArticlesLoadIsFinished ||
current is CatalogArticlesLoadInProgress;
},
builder: (context, state) {
return CatalogArticlesWidget(
data: state.data,
);
},
);
...
From this page, I can navigate to a page that contains this same BlocBuilder and same widget (CatalogArticlesWidget). This second page calls the bloc CatalogBloc to reload data of the same type, but filtered in initState:
#override
void initState() {
context.read<CatalogBloc>().add(CatalogArticlesLoadRequested(family: widget.family));
super.initState();
}
So when I pop to the first screen (from the second), the data has changed.
What is the cleanest way to avoid this kind of behavior ?
Create a new instance of that bloc for the 2nd page
First solution: see w461 answer.
Second solution: in my case, I think it is better to create new states for each page.

Function invoke in Flutter to get attributes from Firebase

I want to define and invoke a function in Flutter to get required values from Firebase.
In the below code I have defined getCourseDetails() function and invoking it in the container by passing a parameter to get the value.
The Course_Details collection has many documents with course_id's which has attribute with name (course_name, subtitle). I use these values to build a listview cards in next steps.
I am able to get the values from the function using async await, but for some reason the values keeps on updating and never stops. It kind of goes to loop and keeps on running. I added print statements to check and it keeps on running and printing.
Please let me know what wrong I am doing here or how to define function here to avoid the issue. Thanks
class _CourseProgressListState extends State<CourseProgressList> {
String course_name, subtitle;
getCourseDetails(course_id_pass) async {
DocumentSnapshot document = await Firestore.instance.collection('Course_Details').document(course_id_pass).get();
setState(() {
course_name = document.data['course_name'];
subtitle = document.data['subtitle'];
});
}
#override
Widget build(BuildContext context) {
return Container(
child: StreamBuilder(
stream: Firestore.instance.collection('Enrolling_Courses').where('student_id', isEqualTo: widget.id).snapshots(),
builder: (context, snapshot) {
if (snapshot.data == null) return Text('no data');
return ListView.builder(
itemCount: snapshot.data.documents.length,
itemBuilder: (_, int index) {
var course_details = snapshot.data.documents[index];
getCourseDetails(course_details['course_id']);
Application Flow
On first build of CourseProgressList the Widget State _CourseProgressListState is loaded. This State in the build method uses a StreamBuilder to retrieve all documents from the firestore. As soon as the documents are received the StreamBuilder attempts to build the ListView using the ListViewBuilder.
In the ListViewBuilder you make the async call to getCourseDetails(course_details['course_id']); which when complete populates two attributes String course_name, subtitle;
The problem starts here
Problem
When you call
setState(() {
course_name = document.data['course_name'];
subtitle = document.data['subtitle'];
});
you trigger a Widget rebuild and so the process starts over again to rebuild the entire widget.
NB. refreshing state of a stateful widget will trigger a widget rebuild
NB. Firestore.instance.collection('Enrolling_Courses').where('student_id', isEqualTo: widget.id).snapshots() returns a stream of realtime changes implying that your List will also refresh each time there is a change to this collection
Recommendations
If you do not need to call the setState try not to call the setState.
You could let getCourseDetails(course_id_pass) return a Future/Stream with the values desired and use another FutureBuilder/StreamBuilder in your ListViewBuilder to return each ListViewItem. Your user may appreciate seeing some items instead of waiting for all the course details to be available
Abstract your request to firestore in a repository/provider or another function/class which will do the entire workload, i.e retrieving course ids then subsequently the course details and returning a Future<List<Course>> / Stream<List<Course>> for your main StreamBuilder in this widget (see reference snippet below as a guide and requires testing)
Reference Snippet
Your abstraction could look something like this but this decision is up to you. There are many software design patterns to consider or you could just start by getting it working.
//let's say we had a class Course
class Course {
String courseId;
String courseName;
String subTitle;
Course({this.courseId,this.courseName,this.subTitle})
}
Stream<List<Course>> getStudentCourses(int studentId){
return Firestore.instance
.collection('Enrolling_Courses')
.where('student_id', isEqualTo: studentId)
.snapshots()
//extract documents from snapshot
.map(snapshot => snapshot?.data ?? [])
//we will then request details for each document
.map(documents =>
/*because this is an asynchronous request for several
items which we are all interested in at the same time, we can wrap
this in
a Future.wait and retrieve the results of all as a list
*/
Future.wait(
documents.map(document =>
//making a request to firestore for each document
Firestore.instance
.collection('Course_Details')
.document(document['course_id'])
.get()
/* making a final transformation turning
each document into a Course item which we can easily pass to our
ListBuilder/Widgets
*/
.then(courseItem => Course(
courseId:document['course_id'],
courseName:
courseItem.data['course_name'],
subTitle:
courseItem.data['subtitle']
)
)
)
)
);
}
References/Resources
FutureBuilder - https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html
StreamBuilder - https://api.flutter.dev/flutter/widgets/StreamBuilder-class.html
Future.wait - https://api.flutter.dev/flutter/dart-async/Future/wait.html

How can I use Provider to provide a bloc to a PageView() without the child resubscribing everytime I switch page?

I am using Provider to provide my bloc to a widget called TheGroupPage via a static create method
static Widget create(BuildContext context, GroupModel group) {
final database = Provider.of<DatabaseService>(context);
return Provider(
create: (_) => GroupMembersBloc(database, group),
child: TheGroupPage(group),
dispose: (BuildContext context, GroupMembersBloc bloc) => bloc.dispose(),
);
}
That widget has a PageView() with 3 pages
PageView(children: [
TheGroupNotificationsView(),
TheGroupMembersView(group),
TheGroupSettingsView(group),
])
The group members view looks for the GroupMembersBloc
GroupMembersBloc bloc = Provider.of<GroupMembersBloc>(context);
I also tried to put listen to false but this did not work. And I want the widget to listen for any changes. The page uses that bloc's stream to draw a list of group members
class GroupMembersBloc{
StreamController<List<UserModel>> _controller = StreamController<List<UserModel>>();
Stream<List<UserModel>> get stream => _controller.stream;
GroupMembersBloc(DatabaseService database, GroupModel group)
{
_controller.addStream(database.groupMembersStream(group));
}
void dispose(){
_controller.close();
}
}
The problem is when I switch page inside the PageView() I get an error on the page after the first time it has been shown. The error says Bad state: Stream has already been listened to. how can I solve this?
That's because stream controllers allow only 1 Subscription (or 1 listener) , you could use the [StreamController<List<UserModel>>.broadcast()][1] constructor instead of StreamController>().
I ended up moving the StreamBuilder to the parent widget above the PageView() which fixed the problem.