Flutter : same BlocBuilder on two consecutive pages - flutter

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.

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.

Flutter Navigator popUntil with Bloc not rebuilding state

I have a 3 step form, For each step I am pushing a new page by passing the Bloc provider wrapped in MaterialPageRoute using Navigator so the same bloc can be accessed on those pages like this:
Navigator.of(context).push(MaterialPageRoute<TeacherTemperature>(
builder: (_) => BlocProvider.value(value: BlocProvider.of<TeacherattendanceclassBloc>(context),
child: TeacherTemperature()))
);
Now on the last step, when the form is completed I am doing popUntil to go back to step 1 and update the state:
Navigator.popUntil(context, ModalRoute.withName("teacherAttendanceClassList"));
BlocProvider.of<TeacherattendanceclassBloc>(context)
.add(StudentsCheckedInCompleted(widget.studentList!.length));
This successfully goes back to step 1 page and also I am able to call the event StudentsCheckedInCompleted event and the bloc is processing the state change as well.
But The step one page is not re-building for some reason? (I have tried to but the condition under the BlocBuilder as well)
This is the step one page, where I'm listening for the state in the BlocListener like this:
BlocListener<TeacherattendanceclassBloc, TeacherattendanceclassState>(
listener: (context, state) {
print(state);
if(state is StudentSelectionUpdated){
childrenSelectedList = state.studentsSelectedList;
}
if(state is StudentsCheckedInSuccessfull){ ----> this is not being triggered on the page
print('StudentsCheckedInSuccessfully is triggered on the teacher attendance page');
}
},
Am I missing somethig?
This issue might be caused by State equatibility check, which means:
Assume this is your state;
class MyState extends Equatable {
final String myVariable1;
final int myVariable2;
MyState({this.myVariable, this.myVariable2});
#override
List<Object> get props => [myVariable, myVariable2];
}
on the bottom, as you see, there is props which helps bloc to decide if state is changed or altered, and if that prop say "i am changed" (by changing any content of it), bloc rebuilds the widget. But if your state is not altered/changed as intended or you forgot to add proper props that will later say to bloc that you edited your state, your widget will not be rebuilt.
Maybe you can can add more details in code to understand the root cause. (Can you please share your bloc, events and your state objects)

Flutter/Dart - Edit Page in PageView, then Refresh and Scroll back to Same Place?

Currently, I can submit edits to a single page in a PageView and then either Navigator.push to a newly created single edited page or Navigator.pop back to the original Pageview containing the unedited page.
But I'd prefer to pop back to the the same place in an updated/refreshed Pageview. I was thinking I could do this on the original PageView page:
Navigator.pushReplacement(context,new MaterialPageRoute(
builder: (BuildContext context) => EditPage()),);
But after editing, how can I pop back to a refreshed PageView which is scrolled to the now updated original page? Or is there a better way? Someone mentioned keys, but I've not yet learned to use them.
The question deals with the concept of Reactive App-State. The correct way to handle this is through having an app state management solution like Bloc or Redux.
Explanation: The app state takes care of the data which you are editing. the EditPage just tells the store(App-State container) to edit that data and the framework takes care of the data that should be updated in the PageView.
as a temporary solution you can use an async call to Navigation.push() and refresh the PageView State once the EditPage comes back. you can also use an overloaded version of pop() to return a success condition which aids for a conditional setState().
Do you know that Navigator.pushReplacement(...) returns a Future<T> which completes when you finally return to original context ?
So how are you going to utilize this fact ?
Lets say you want to update a String of the original page :
String itWillBeUpdated="old value";
#override
Widget build(BuildContext ctx)
{
.
.
.
onPressesed:() async {
itWillBeUpdated= await Navigator.pushReplacement(context,new MaterialPageRoute(
builder: (BuildContext context) => EditPage()),);
setState((){});
},
}
On your editing page , you can define Navigator.pop(...) like this :
Navigator.pop<String>(context, "new string");
by doing this , you can provide any data back to the original page and by calling setState((){}) , your page will reflect the changes
This isn't ideal, but works somewhat. First I created a provider class and added the following;
class AudioWidgetProvider with ChangeNotifier {
int refreshIndex;
setRefreshIndex (ri) {
refreshIndex = ri;
return refreshIndex;
}
}
Then in my PageView Builder on the first page, I did this;
Widget build(context) {
var audioWidgetProvider = Provider.of<AudioWidgetProvider>(context);
return
PreloadPageView.builder(
controller: PreloadPageController(initialPage: audioWidgetProvider.refreshIndex?? 0),
Then to get to the EditPage (2nd screen) I did this;
onPressed: () async {
audioWidgetProvider.setRefreshIndex(currentIndex);
Navigator.pushReplacement(context,new MaterialPageRoute(builder: (BuildContext context) => EditPage()),); }
And finally I did this to return to a reloaded PageView scrolled to the edited page;
Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) =>HomePage()));
The only problem now is that the PageView list comes from a PHP/Mysql query and I'm not sure what to do if new items are added to the list from the Mysql database. This means the currentIndex will be wrong. But I guess that's the topic of another question.

How to return from a widget to StreamBuilder after navigating to another page in flutter app?

So the first picture has codes from the first page where I have a StreamBuilder in my flutter app, there I tried navigating to the stateful widget named PickupScreen containing the code in second picture and it gave me an error which says SetState( or markNeedsbuild() called during build.So I called the widget directly and it worked but the problem is I cannot go the previous page I know I can't use pop but I need a solution for returning to previous page and show the else code.Currently I am Navigating to another page named HomePage.
You can't do it like that. Either you manage your screen state based on your widget.channel.stream or you must design your UI in a different way so you can actually push a new screen on your screen 1 and then you will be able to pop.
If you still want to keep it like that, you can do something like the following in your first screen:
return StreamBuilder(
stream: widget.channel.stream,
builder: (context, snapshot){
if(snapshot.hasData && jsonDecode(snapshot.data['ntype'] == 10){
WidgetsBinding.instance.addPostFrameCallback((_) => Navigator.push(context, MaterialPageRoute(builder: (_) => PickupScreen(data: jsonDecode(snapshot.data))));
return const SizedBox();
} else {
return _isLoading ? LoadingScreen() : Scaffold();
}
});
Also, next time, make sure you copy/paste code instead of screenshots to make it easy to edit with modifications. :)

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.