With a state change observer I can see that my bloc changes its states to LoadInProgress and back to PmLoadSuccess. However, the widget is not called with the change to LoadInProgress. I have a breakpoint in the BlocBuilder, however, the code stops there only when PmLoadSuccess is emitted.
My state definition
#freezed
class ProblemManagerState with _$ProblemManagerState {
const factory ProblemManagerState.initial() = PmInitial;
const factory ProblemManagerState.loadInProgress() = PmLoadInProgress;
const factory ProblemManagerState.loadSuccess(
Problem problem) = PmLoadSuccess;
) = PmSyncFailed;
}
The bloc
#injectable
class ProblemManagerBloc extends Bloc<ProblemManagerEvent, ProblemManagerState> {
final IProblemsRepository problemsRepository;
ProblemManagerBloc(this.problemsRepository) : super(PmInitial());
#override
Stream<ProblemManagerState> mapEventToState(
ProblemManagerEvent event,
) async* {
yield PmLoadInProgress(); // <<< does not emit to widget
yield* event.map(
pmProblemRequested: (e) async* {
yield ProblemManagerState.loadInProgress(); // <<< doesn't emit either
print (state);
yield* _mapPmProblemRequestedToState(e);
},
);
}
Stream<ProblemManagerState> _mapPmProblemRequestedToState(/*PmProblemRequested*/dynamic event) async* {
dynamic nextProblem;
late int problemClassId;
problemClassId = event.problemClassId!;
}
final String rootId = await _getNextProblemRootId(problemClassId);
final Either<StorageFailure, Problem> failureOrProblem = await problemsRepository.getProblem(rootId);
yield failureOrProblem.fold(
(fail) => PmLoadFailure(fail),
(load) => PmLoadSuccess(load)); // <<< yield does emit to widget
}
The widget (calling another widget if load successful)
class ProblemManagerPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocBuilder<ProblemManagerBloc, ProblemManagerState>(
builder: (context, state) {
return state.map( // <<< breakpoint not reached with PmLoadInProgress (=ProblemManagerState.loadInProgress)
initial: (_) => Container(),
loadInProgress: (_) => const Center(
child: CircularProgressIndicator(),
),
loadSuccess: (state) => PmGateway(state.problem),
);
}
);
}
}
Taken from github issue list
This is expected because BlocBuilder can only rebuild at 60fps. In this case you are emitting states faster than BlocBuilder can rebuild.
Related
I am getting this error when I have signed out from my flutter app and trying to log in again:
StateError (Bad state: Stream has already been listened to.)
The code that gives me this error is on my first page:
#override
void initState() {
AwesomeNotifications().actionStream.listen((notification) async {
if (notification.channelKey == 'scheduled_channel') {
var payload = notification.payload['payload'];
var value = await FirebaseFirestore.instance
.collection(widget.user.uid)
.doc(payload)
.get();
navigatorKey.currentState.push(PageRouteBuilder(
pageBuilder: (_, __, ___) => DetailPage(
user: widget.user,
i: 0,
docname: payload,
color: value.data()['color'].toString(),
createdDate: int.parse((value.data()['date'].toString())),
documentId: value.data()['documentId'].toString(),)));
}
});
super.initState();
}
And on another page that contains the sign out code.
await FirebaseAuth.instance.signOut();
if (!mounted) return;
Navigator.pushNamedAndRemoveUntil(context,
"/login", (Route<dynamic> route) => false);
What can I do to solve this? Is it possible to stop listen to actionstream when I log out? Or should I do it in another way?
Streams over all are single use, they replace the callback hell that that ui is, at first a single use streams can seem useless but that may be for a lack of foresight. Over all (at lest for me) flutter provides all the necessary widgets to not get messy with streams, you can find them in the Implementers section of ChangeNotifier and all of those implement others like TextEditingController.
With that, an ideal (again, at least for me) is to treat widgets as clusters where streams just tie them in a use case, for example, the widget StreamBuilder is designed to build on demand so it only needs something that pumps changes to make a "live object" like in a clock, a periodic function adds a new value to the stream and the widget just needs to listen and update.
To fix your problem you can make .actionStream fit the case you are using it or change a bit how are you using it (having a monkey patch is not good but you decide if it is worth it).
This example is not exactly a "this is what is wrong, fix it", it is more to showcase a use of how pushNamedAndRemoveUntil and StreamSubscription can get implemented. I also used a InheritedWidget just because is so useful in this cases. One thing you should check a bit more is that the variable count does not stop incrementing when route_a is not in focus, the stream is independent and it will be alive as long as the widget is, which in your case, rebuilding the listening widget is the error.
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(App());
const String route_a = '/route_a';
const String route_b = '/route_b';
const String route_c = '/route_c';
class App extends StatelessWidget {
Stream<int> gen_nums() async* {
while (true) {
await Future.delayed(Duration(seconds: 1));
yield 1;
}
}
#override
Widget build(BuildContext ctx) {
return ReachableData(
child: MaterialApp(
initialRoute: route_a,
routes: <String, WidgetBuilder>{
route_a: (_) => Something(stream: gen_nums()),
route_b: (_) => FillerRoute(),
route_c: (_) => SetMount(),
},
),
);
}
}
class ReachableData extends InheritedWidget {
final data = ReachableDataState();
ReachableData({super.key, required super.child});
static ReachableData of(BuildContext ctx) {
final result = ctx.dependOnInheritedWidgetOfExactType<ReachableData>();
assert(result != null, 'Context error');
return result!;
}
#override
bool updateShouldNotify(ReachableData old) => false;
}
class ReachableDataState {
String? mount;
}
// route a
class Something extends StatefulWidget {
// If this widget needs to be disposed then use the other
// constructor and this call in the routes:
// Something(subscription: gen_nums().listen(null)),
// final StreamSubscription<int> subscription;
// Something({required this.subscription, super.key});
final Stream<int> stream;
Something({required this.stream, super.key});
#override
State<Something> createState() => _Something();
}
class _Something extends State<Something> {
int count = 0;
void increment_by(int i) => setState(
() => count += i,
);
#override
void initState() {
super.initState();
widget.stream.listen(increment_by);
// To avoid any funny errors you should set the subscription
// on pause or the callback to null on dispose
// widget.subscription.onData(increment_by);
}
#override
Widget build(BuildContext ctx) {
var mount = ReachableData.of(ctx).data.mount ?? 'No mount';
return Scaffold(
body: InkWell(
child: Text('[$count] Push Other / $mount'),
onTap: () {
ReachableData.of(ctx).data.mount = null;
Navigator.of(ctx).pushNamed(route_b);
},
),
);
}
}
// route b
class FillerRoute extends StatelessWidget {
const FillerRoute({super.key});
#override
Widget build(BuildContext ctx) {
return Scaffold(
body: InkWell(
child: Text('Go next'),
// Option 1: go to the next route
// onTap: () => Navigator.of(ctx).pushNamed(route_c),
// Option 2: go to the next route and extend the pop
onTap: () => Navigator.of(ctx)
.pushNamedAndRemoveUntil(route_c, ModalRoute.withName(route_a)),
),
);
}
}
// route c
class SetMount extends StatelessWidget {
const SetMount({super.key});
#override
Widget build(BuildContext ctx) {
return Scaffold(
body: InkWell(
child: Text('Set Mount'),
onTap: () {
ReachableData.of(ctx).data.mount = 'Mounted';
// Option 1: pop untill reaches the correct route
// Navigator.of(ctx).popUntil(ModalRoute.withName(route_a));
// Option 2: a regular pop
Navigator.of(ctx).pop();
},
),
);
}
}
I am facing one issue in which when I am going from one page to another page using flitter BLoC, my first page rebuild before reaching to second. I am able to restrict the rebuild of the page using buildWhen in BlocBuilder, but the problem is when I come back to the first page again by back press then the page can not show the previous state widgets. I don't know how to manage navigation between pages without rebuild the page again, I am using flutter_bloc 6.1.1 below is my code.
FirstPage
class FirstPage extends StatefulWidget {
final MyData dataObj;
FirstPage({this.dataObj});
#override
_MyFirstPageState createState() => _MyFirstPageState();
}
class _MyFirstPageState extends State<FirstPage> {
FirstPageBloc _bloc = FirstPageBloc();
String _userAddress='';
#override
void initState() {
super.initState();
_bloc.add(UserInfoEvent(dataObj:widget.dataObj));
}
#override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomPadding: false,
appBar: AppBar(
title: Text(StringConstants.APP_TITLE_HEADING),
),
body: BlocListener<FirstPageBloc, FirstPageState>(
cubit: _bloc,
listenWhen: (previousState, state) {
// return true/false to determine whether or not
// to call listener with state
return true;
},
listener: (context, state) async{
if (state is LoadingState) {
print('Loading ...');
}
if (state is DataInfoState) {
_userAddress=state.userAddress;
}
if(state is ConfirmationState){
Navigator.push(context, MaterialPageRoute(builder: (context) => SecondPage(dataObj: widget.dataObj)));
}
},
child: BlocBuilder<FirstPageBloc, FirstPageState>(
//bloc: _bloc,
cubit: _bloc,
buildWhen: (previousState, state) {
// return true/false to determine whether or not
// to rebuild the widget with state
if(state is ConfirmationState){
return false;
}
return true;
},
builder: (context, state) {
if (state is LoadingState) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
CircularProgressIndicator(valueColor:
AlwaysStoppedAnimation<Color>(ColorConstants.Primary),),
Text(StringConstants.PLEASE_WAIT)
],),
);
}
return _mainWidget();
}),
),
);
}
}
BLoC
class FirstPageBloc extends Bloc<FirstPageEvent, FirstPageState>{
FirstPageBloc() : super(InitialState());
#override
Stream<FirstPageState> mapEventToState(FirstPageEvent event) async*{
// TODO: implement mapEventToState
if(event is DataInfoEvent){
yield* _getUserData(event.dataObj);
}
if(event is ConfirmationEvent){
yield* _confirmTaskData(event.dataObj);
}
}
Stream<DelConfirmState> _confirmTaskData(MyData dataObj) async* {
yield LoadingState();
//Performing some SQLite DB operations
yield ConfirmationState();
}
Stream<DelConfirmState> _getUserData(MyData dataObj) async* {
yield LoadingState();
String userAddress='ABDC001, PIN- 0091910, 5th Main USA';
//Fetching User data from SQLite database and passing to UI
yield DataInfoState(userAddress:userAddress);
}
}
State
abstract class FirstPageState extends Equatable {}
///This is our initial state
class InitialState extends FirstPageState {
#override
List<Object> get props => null;
}
//This state will call for loading the progress var
class LoadingState extends FirstPageState {
#override
List<Object> get props => [];
}
//This state will call for loading the progress var
class ErrorState extends FirstPageState {
final String errorMessage;
ErrorState({#required this.errorMessage});
#override
List<Object> get props => [];
}
//This state will retun the userdata
class DataInfoState extends FirstPageState {
final String userAddress;
DataInfoState({#required this.userAddress});
#override
// TODO: implement props
List<Object> get props => [];
}
class TaskConfirmationState extends FirstPageState {
ConfirmationState({});
#override
// TODO: implement props
List<Object> get props => [];
}
Event
abstract class FirstPageEvent extends Equatable {}
class GetUserInfoEvent extends FirstPageEvent {
final MyData dataObj;
GetUserInfoEvent({this.taskObj});
#override
List<Object> get props => [];
}
class ConfirmationEvent extends FirstPageEvent {
final MyData dataObj
ConfirmationEvent({this.dataObj});
#override
List<Object> get props => [];
}
Please advise
Thank You
You need to provide your bloc at a higher level widget, then you need to get it from the context. In this way the state will persist even through navigation.
You can do that wrapping your widget like this:
BlocProvider(
create: (context) => FirstPageBloc(),
child: FirstPage(),
)
and then inside of initState you can get it like this:
_bloc = BlocProvider.of<FirstPageBloc>(context);
I have a BlocBuilder which handles building widgets depending on the yielded state for my dashboard page.
body: BlocBuilder<DashboardBloc, DashboardState>(
builder: (context, state) {
print(state);
if (state is DashboardInitial) {
return loadingList();
} else if (state is DashboardEmpty) {
return emptyList();
} else if (state is DashboardLoaded) {
return loadedList(context, state);
}
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.push(
context, MaterialPageRoute(builder: (context) => AddPage()));
},
I want to be able to navigate to the add page, fill in some textfields, and then dispatch an event to my dashboard bloc, with the idea being that upon navigating back to the dashboard, my list will be updated.
class AddPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
TextEditingController titleController = TextEditingController();
TextEditingController descriptionController = TextEditingController();
return Scaffold(
appBar: AppBar(title: Text('Add')),
body: Container(
padding: EdgeInsets.all(10),
child: Column(
children: [
TextField(
controller: titleController,
),
TextField(
controller: descriptionController,
),
RaisedButton(onPressed: () {
BlocProvider.of<DashboardBloc>(context)
.add(DashboardWorryAdded('title', 'description'));
}),
],
),
),
);
}
}
When following the code using breakpoints, i am able to see that my state is yielded in the 'mapeventtostate' function, however my dashboard is never rebuilt with the new values.
Here is the code for my Bloc, events, and states. My first thought would be that Equatable was detecting the same state being returned, but upon removing Equatable, my problem is still persists.
#override
Stream<DashboardState> mapEventToState(
DashboardEvent event,
) async* {
if (event is DashboardWorryAdded) {
yield* _mapDashboardWorryAddedToState(event);
} else if (event is DashboardLoading) {
yield* _mapDashboardLoadingToState(event);
} else if (event is AppStarted) {
yield* _mapAppStartedToState(event);
}
}
Stream<DashboardState> _mapAppStartedToState(AppStarted event) async* {
List<Worry> _wList = await repo.getAllWorries();
if (_wList.length != 0) {
yield DashboardLoaded(worryList: _wList);
} else {
yield DashboardEmpty();
}
}
Stream<DashboardState> _mapDashboardLoadingToState(
DashboardLoading event) async* {
List<Worry> _wList = await repo.getAllWorries();
if (_wList != 0) {
yield DashboardLoaded(worryList: _wList);
} else {
yield DashboardEmpty();
}
}
Stream<DashboardState> _mapDashboardWorryAddedToState(
DashboardWorryAdded event) async* {
await repo.addWorry(event.title, event.description);
List<Worry> worryList = List<Worry>();
worryList = await repo.getAllWorries();
yield DashboardLoaded(worryList: worryList);
}
}
#immutable
abstract class DashboardEvent {}
class DashboardLoading extends DashboardEvent {
DashboardLoading();
}
class DashboardWorryAdded extends DashboardEvent {
final String title, description;
DashboardWorryAdded(this.title, this.description);
}
class AppStarted extends DashboardEvent {
AppStarted();
}
#immutable
abstract class DashboardState {}
class DashboardInitial extends DashboardState {
DashboardInitial();
}
class DashboardLoaded extends DashboardState {
final List<Worry> worryList;
DashboardLoaded({this.worryList});
}
class DashboardEmpty extends DashboardState {
DashboardEmpty();
}
Instead of trying to mutate another page's state (a bit of a no-no where state management is concerned), take advantage of the fact that the push method of the navigator returns a future that completes when that page gets popped, and as a bonus, the value of the future will include the value that was given to the pop method in the other page. So you can now do something like this:
class DashboardBloc {
...
void showAddPage() async {
// Do this to retrieve the value passed to the add page's call to `pop`
final value = await Navigator.of(context).push(...);
// Do this if the add page doesn't return a value in `pop`
await Navigator.of(context).push(...);
// Either way, you can now refresh your state in response to
// the add page popping
emit(...);
}
}
Note: This works just as well for named routes too.
I'm creating an app where you login and go to a page where you have a list of your restaurants, you have also a form where you can add a new restaurant.
This part works.
The problem is that when i click add the restaurant is added in firestore correctly, but the list doesn't refresh. I usually yield 2 states, a LoadingState and a LoadedRestaurantsListState, but with the last version of flutter_bloc this trick doesn't work, seems like just the last state yielded is received, but the previous was LoadedRestaurantsListState, so they are equals and the blocbuilder ignores the second one. So I've to use the equatable's props to distinguish the 2 states, but in the equatable documentation is written: "Note: Equatable is designed to only work with immutable objects so all member variables must be final".
So I've to make all the model's fields final, but if I do it how can i modify just one o two fields when I need it to?
What is the best practice?
If someone has examples, or videos, etc it would be very appreciated.
Thanks in advance
Without props
FirebaseBloc.dart
Stream<FirebaseState> mapEventToState(
FirebaseEvent event,
) async* {
print("event firebase ${event.runtimeType.toString()}");
if (event is CreateRestaurantFirebaseEvent) {
yield LoadingState();
await _databaseService.createRestaurant(event.restaurant, event.user);
List<Restaurant> restaurantsList = await _databaseService
.loadRestaurantsList(event.user.restaurantsIDsList);
yield LoadedRestaurantsListState(restaurantsList);
}
if (event is LoadRestaurantsListEvent) {
List<Restaurant> restaurantsList =
await _databaseService.loadRestaurantsList(event.restaurantsIDs);
yield LoadedRestaurantsListState(restaurantsList);
}
FirebaseState.dart
class LoadingState extends FirebaseState {
#override
List<Object> get props => [];
}
class LoadedRestaurantsListState extends FirebaseState {
List<Restaurant> restaurantsList;
LoadedRestaurantsListState(this.restaurantsList);
#override
List<Object> get props => [];
}
view.dart
class RestaurantSelectionScreen extends StatefulWidget {
final User user;
RestaurantSelectionScreen({
#required this.user,
});
#override
_RestaurantSelectionScreenState createState() =>
_RestaurantSelectionScreenState();
}
class _RestaurantSelectionScreenState extends State<RestaurantSelectionScreen> {
FirebaseBloc _firebaseBloc;
#override
void initState() {
super.initState();
_firebaseBloc = FirebaseBloc();
_firebaseBloc.add(LoadRestaurantsListEvent(widget.user.restaurantsIDsList));
}
#override
Widget build(BuildContext context) {
return BlocProvider<FirebaseBloc>(
create: (context) => _firebaseBloc,
child: Scaffold(
body: SingleChildScrollView(
child: Center(
child: BlocBuilder(
cubit: _firebaseBloc,
builder: (context, state) {
print("state ${state.runtimeType.toString()}");
if (state is InitialFirebaseState) {
return CircularProgressIndicator();
} else if (state is LoadedRestaurantsListState) {
return buildUI(state);
} else if (state is LoadingState) {
return CircularProgressIndicator();
} else {
return _CreateRestaurantFormWidget(widget.user);
}
},
),
),
),
),
);
}
Recently I am learning flutter_bloc, and I refer to the project flutter_weather.
What I am puzzled is that if a Bloc class has many Events, and most of the Events will have values returned by State, and there are many BlocBuilders in the project, what should I do if I want a BlocBuilder to only respond to a certain Event?
The method I can think of is to divide this Bloc into multiple Blocs, or treat each value to be returned as an attribute of Bloc, BlocBuilder uses the buildwhen method to determine whether to rebuild.
But both of these methods are not good for me. Is there any good method? It is best to have projects on github for reference.
For example:
This is Event:
abstract class WeatherEvent extends Equatable {
const WeatherEvent();
}
class WeatherRequested extends WeatherEvent {
final String city;
const WeatherRequested({#required this.city}) : assert(city != null);
#override
List<Object> get props => [city];
}
class WeatherRefreshRequested extends WeatherEvent {
final String city;
const WeatherRefreshRequested({#required this.city}) : assert(city != null);
#override
List<Object> get props => [city];
}
This is State:
abstract class WeatherState extends Equatable {
const WeatherState();
#override
List<Object> get props => [];
}
class WeatherInitial extends WeatherState {}
class WeatherLoadInProgress extends WeatherState {}
class WeatherLoadSuccess extends WeatherState {
final Weather weather;
const WeatherLoadSuccess({#required this.weather}) : assert(weather != null);
#override
List<Object> get props => [weather];
}
class WeatherLoadFailure extends WeatherState {}
This is Bloc:
class WeatherBloc extends Bloc<WeatherEvent, WeatherState> {
final WeatherRepository weatherRepository;
WeatherBloc({#required this.weatherRepository})
: assert(weatherRepository != null),
super(WeatherInitial());
#override
Stream<WeatherState> mapEventToState(WeatherEvent event) async* {
if (event is WeatherRequested) {
yield* _mapWeatherRequestedToState(event);
} else if (event is WeatherRefreshRequested) {
yield* _mapWeatherRefreshRequestedToState(event);
}
}
Stream<WeatherState> _mapWeatherRequestedToState(
WeatherRequested event,
) async* {
yield WeatherLoadInProgress();
try {
final Weather weather = await weatherRepository.getWeather(event.city);
yield WeatherLoadSuccess(weather: weather);
} catch (_) {
yield WeatherLoadFailure();
}
}
Stream<WeatherState> _mapWeatherRefreshRequestedToState(
WeatherRefreshRequested event,
) async* {
try {
final Weather weather = await weatherRepository.getWeather(event.city);
yield WeatherLoadSuccess(weather: weather);
} catch (_) {}
}
}
This is BlocConsumer:
// BlocBuilder1
BlocBuilder<WeatherBloc, WeatherState>(
builder: (context, state) {
if (state is WeatherLoadInProgress) {
return Center(child: CircularProgressIndicator());
}
if (state is WeatherLoadSuccess) {
final weather = state.weather;
return Center(child: Text("WeatherRequested "))
}
)
// BlocBuilder2
BlocBuilder<WeatherBloc, WeatherState>(
builder: (context, state) {
if (state is WeatherLoadInProgress) {
return Center(child: CircularProgressIndicator());
}
if (state is WeatherLoadSuccess) {
final weather = state.weather;
return Center(child: Text("WeatherRefreshRequested"))
}
)
The problem is that I want BlocBuilder1 only to work when the type of Event is WeatherRequested and BlocBuilder2 only works when the type of Event is WeatherRefreshRequested. One of my ideas is that each Event has its own State, and then judge the type of State in buildwhen.
Is there any good method?
if you want to build you widget to respond for certain states you should use
BlocConsumer and tell that bloc in buildWhen to tell it what state it should build/rebuild you widget on.
BlocConsumer<QuizBloc, QuizState>(
buildWhen: (previous, current) {
if (current is QuizPoints)
return true;
else
return false;
},
listener: (context, state) {},
builder: (context, state) {
if (state is QuizPoints)
return Container(
child: Center(
child: Countup(
begin: 0,
end: state.points,
duration: Duration(seconds: 2),
separator: ',',
),
),
);
else
return Container();
},
);