Flutter Navigator popUntil with Bloc not rebuilding state - flutter

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)

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 Riverpod: using ref.watch in the constructor for a ChangeNotifier, how to avoid Notifier being recreated upon changes

I have a ChangeNotifier, which I use with ChangeNotifierProvider to track the state of a screen in my app. It has a constructor:
CategoryViewState(ProviderRefBase ref, int listId) {
subjects = ref.watch(otherProvider.select((value) => value.getSubjects()));
}
The problem I'm encountering is that when otherProvider.getSubjects() changes, the whole ChangeNotifier is recreated from scratch, rather than the subjects list being updated. This means the state of the page is lost.
Is there a fix or another way to do this that avoids this happening?
Just put the widget that list to the changes in a Consumer widget and watch changes inside it:
Consumer(builder: (context, ref, child) {
final subjects = ref.watch(otherProvider.select((value) => value.getSubjects()));
return YourWidget(); // Just this widget will be rebuilt
},)

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.

Flutter + Bloc 6.0.6. BlocBuilder's "builder" callback isn't provided with the same state as the "buildWhen" callback

I'm building a tic-tak-toe app and I decided to learn BLoC for Flutter along. I hava a problem with the BlocBuilder widget.
As I think about it. Every time Cubit/Bloc that the bloc builder widget listens to emits new state the bloc builder goes through this routine:
Call buildWhen callback passing previous state as the previous parameter and the newly emitted state as the current parameter.
If the buildWhen callback returned true then rebuild.
During rebuilding call the builder callback passing given context as context parameter and the newly emitted state as state parameter. This callback returns the widget that we return.
So the conclusion is that the current parameter of the buildWhen call is always equal to the state parameter of the builder call. But in practice it's different:
BlocBuilder<GameCubit, GameState>(
buildWhen: (previous, current) => current is SetSlotSignGameState && (current as SetSlotSignGameState).slotPosition == widget.pos,
builder: (context, state) {
var sign = (state as SetSlotSignGameState).sign;
// Widget creation goes here...
},
);
In the builder callback, it throws:
The following _CastError was thrown building BlocBuilder<GameCubit, GameState>(dirty, state:
_BlocBuilderBaseState<GameCubit, GameState>#dc100):
type 'GameState' is not a subtype of type 'SetSlotSignGameState' in type cast
The relevant error-causing widget was:
BlocBuilder<GameCubit, GameState>
The method where I emit the states that is in the GameCubit class:
// [pos] is the position of the slot clicked
void setSlotSign(Vec2<int> pos) {
// Some code
emit(SetSlotSignGameState(/* Parameter representing the sign that is being placed in the slot*/, pos));
// Some code
emit(TurnChangeGameState());
}
Briefly about types of states. SetSlotSignGameState is emitted when a user taps on a slot in the tic-tac-toe grid and the slot is empty. So this state means that we need to change sign in some slot. TurnChangeGameState is emitted when we need to give the turn to the next player.
Temporary solution. For now I fixed it by saving the state from buildWhen callback in a private field of the widget's state and then using it from the builder. BlocListener also has this problem but there I can just move the check from listenWhen callback into listen callback. The disadvantage of this solution is that it's very inelegant and inconvenient.
buildWhen is bypassed (not even called) on initial state OR when Flutter requests a rebuild.
I have created a small "test" to emphasize that:
import 'package:bloc/bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
void main() {
runApp(BlocTestApp());
}
class BlocTestApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: BlocProvider<TestCubit>(
// Create the TestCubit and call test() event right away
create: (context) => TestCubit()..test(),
child: BlocBuilder<TestCubit, String>(
buildWhen: (previous, current) {
print("Call buildWhen(previous: $previous, current: $current)");
return false;
},
builder: (context, state) {
print("Build $state");
return Text(state);
},
),
),
);
}
}
class TestCubit extends Cubit<String> {
TestCubit() : super("Initial State");
void test() {
Future.delayed(Duration(seconds: 2), () {
emit("Test State");
});
}
}
OUTPUT:
I/flutter (13854): Build Initial State
I/flutter (13854): Call buildWhen(previous: Initial State, current: Test State)
As can be seen from output the initial state is built right away without calling buildWhen. Only when the state changes buildWhen is examined.
Other References
This behavior is also outlined here by the creator of Flutter Bloc library (#felangel):
This is expected behavior because there are two reasons for a
BlocBuilder to rebuild:
The bloc state changed
Flutter marked the widget as needing to be rebuilt.
buildWhen will prevent builds triggered by 1 but not by 2. In
this case, when the language changes, the whole widget tree is likely
being rebuilt which is why the BlocBuilder is rebuilt despite
buildWhen.
Possible solution
In your situation, based on the little code you revealed, is better to store the entire Tic-Tac-Toe configuration in the state and use BLOC events to alter it. In this way you do not need that buildWhen condition.
OR make the check inside the builder function if the logic let you do that (this is the most common used solutions with BLOC).
To respond to you question (if not clear so far :D): Sadly, you can not rely on buildWhen to filter the state types sent to builder function.
Could you please check if SetSlotSignGameState extends the abstract class GameState

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.