Hi there, first of all let me thank you for the awesome libraby.
Im facing an issue with my bloc.The issue is that when the app loads i am able to dispatch an event to the Bloc once,and subsequent calls to dispatch just do nothing.
the app uses the hidden_drawer_menu plugin which i forked and tweaked a bit to allow navigation.It is the same principle as the hidden drawer menu from the fluttery challenges.
I dispatch events to the bloc to notify other parts of the app when there is a screen change thus i can change the drawer menu icon to an arrow_back.
here is the code.
NavigationBloc.dart
import 'package:bloc/bloc.dart';
import 'package:flutter/material.dart';
import './events/navigation_events.dart';
import './states/navigation_state.dart';
import 'dart:async';
class NavigationBloc extends Bloc<NavigationEvents,NavigationState>{
@override
// TODO: implement initialState
NavigationState get initialState => HomeRouteState();
@override
Stream<NavigationState> mapEventToState(NavigationState currentState, NavigationEvents event) async*{
debugPrint("out event:$event");
if(event is GoHomeEvent){
debugPrint('event:$event');
yield HomeRouteState();
}else if(event is GoLoginEvent){
debugPrint('event:$event');
yield LoginRouteState();
}
}
}
NavigationEvents.dart
import 'package:meta/meta.dart';
import 'package:equatable/equatable.dart';
abstract class NavigationEvents extends Equatable{
NavigationEvents([List props=const[]]):super(props);
}
class GoHomeEvent extends NavigationEvents{
@override
String toString() => 'Go home';
}
class GoLoginEvent extends NavigationEvents{
@override
String toString()=>'Go Login Chap Chap';
}
//class
NavigationState.dart
import 'package:equatable/equatable.dart';
abstract class NavigationState extends Equatable{}
class HomeRouteState extends NavigationState{
@override
String toString() {
// TODO: implement toString
return 'home';
}
}
class LoginRouteState extends NavigationState{
@override
String toString() {
// TODO: implement toString
return 'loginchapchap';
}
}
main.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import './src/app.dart';
import 'package:bloc/bloc.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class SimpleBlocDelegate extends BlocDelegate{
@override
void onTransition(Transition transition) {
print(transition);
super.onTransition(transition);
}
}
void main(){
BlocSupervisor().delegate=SimpleBlocDelegate();
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]).then((_){
runApp(App());
});
}
ScreenNavigator.dart the folowing class has a static method which accept events and the navigation bloc to switch screens
import 'package:flutter/material.dart';
import 'package:hidden_drawer_menu/simple_hidden_drawer/provider/simple_hidden_drawer_provider.dart';
import 'package:hidden_drawer_menu/menu/item_hidden_menu.dart';
import 'package:hidden_drawer_menu/hidden_drawer/screen_hidden_drawer.dart';
import 'package:nsiaviemobile/src/blocs/events/navigation_events.dart';
import 'package:nsiaviemobile/src/blocs/navigation_bloc.dart';
import 'package:nsiaviemobile/src/screens/chapchap_loginScreen.dart';
import 'package:nsiaviemobile/src/screens/homeScreen.dart';
import 'package:nsiaviemobile/src/utils/app_assets.dart';
class ScreenNavigator {
NavigationBloc bloc=NavigationBloc();
static List<ScreenHiddenDrawer> drawerItems = [
ScreenHiddenDrawer(
ItemHiddenMenu(
name: "Accueil",
colorTextSelected: Colors.white,
colorLineSelected: NsiaAssets.jaune,
),
HomeScreen()),
ScreenHiddenDrawer(
ItemHiddenMenu(
name: "Nsia Chap Chap",
colorTextSelected: Colors.white,
colorLineSelected: NsiaAssets.jaune,
),
LoginScreen()),
];
static void goTo(BuildContext context, String route, NavigationBloc bloc,
NavigationEvents event) {
switch (route) {
case "home":
bloc.dispatch(event);
SimpleHiddenDrawerProvider.of(context).setScreenByIndex(0, false);
debugPrint('$event dispatched $bloc');
break;
case "login":
bloc.dispatch(event);
SimpleHiddenDrawerProvider.of(context).setScreenByIndex(1, false);
debugPrint('$event dispatched $bloc');
break;
}
}
}
main_drawer.dart is the hidden_drawer itself and is the root widget of the app,i use the bloc here to change the menu icon to a back button as switching away from the root screen which is homeScreen.dart suppose you are on a sub page
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hidden_drawer_menu/hidden_drawer/hidden_drawer_menu.dart';
import 'package:nsiaviemobile/src/blocs/events/navigation_events.dart';
import 'package:nsiaviemobile/src/blocs/navigation_bloc.dart';
import 'package:nsiaviemobile/src/blocs/states/navigation_state.dart';
import 'package:nsiaviemobile/src/utils/screen_navigator.dart';
import '../screens/pubscreen.dart';
import '../utils/app_assets.dart';
class RootDrawer extends StatefulWidget {
final Widget child;
RootDrawer({Key key, this.child}) : super(key: key);
_RootDrawerState createState() => _RootDrawerState();
}
class _RootDrawerState extends State<RootDrawer>
with SingleTickerProviderStateMixin {
NavigationBloc routeBloc;
Animation<double> drawerIconAnimation;
AnimationController drawerCtrl;
void initState() {
drawerCtrl =
AnimationController(duration: Duration(milliseconds: 350), vsync: this);
drawerIconAnimation = Tween<double>(begin: 1.0, end: 0.0).animate(
CurvedAnimation(parent: drawerCtrl, curve: Curves.fastOutSlowIn));
super.initState();
}
Widget _buildMenuIcon(bool isRoot,BuildContext context,NavigationBloc bloc,String screen,NavigationEvents event) {
if (isRoot) {
debugPrint('in root');
return AnimatedBuilder(
animation: drawerIconAnimation,
builder: (context, child) {
return Stack(
children: <Widget>[
Transform.scale(
scale: drawerIconAnimation.value - 1.0,
child: Icon(
Icons.arrow_back,
color: NsiaAssets.bleu,
),
),
Transform.scale(
scale: drawerIconAnimation.value,
child: Icon(
Icons.menu,
color: NsiaAssets.bleu,
),
),
],
);
},
);
}else{
return AnimatedBuilder(
animation: drawerIconAnimation,
builder: (context, child) {
return Stack(
children: <Widget>[
Transform.scale(
scale: drawerIconAnimation.value - 1.0,
child: Icon(
Icons.menu,
color: NsiaAssets.bleu,
),
),
Transform.scale(
scale: drawerIconAnimation.value,
child: IconButton(
onPressed:(){
//override default vehavior of hidden_drawer_navigator
ScreenNavigator.goTo(context, screen, bloc,event);
debugPrint('back pressed');
},
icon: Icon(
Icons.arrow_back,
color: NsiaAssets.bleu,
),
),
),
],
);
});
}
}
@override
Widget build(BuildContext context) {
routeBloc = BlocProvider.of<NavigationBloc>(context);
return Stack(
children: <Widget>[
HiddenDrawerMenu(
transparentAppBar: true,
initPositionSelected: 0,
screens: ScreenNavigator.drawerItems,
enablePerspective: true,
whithAutoTittleName: false,
backgroundColorMenu: NsiaAssets.bleu,
iconMenuAppBar: BlocBuilder<NavigationEvents, NavigationState>(
bloc: routeBloc,
builder: (BuildContext context, NavigationState state) {
debugPrint('state is $state');
if (state is HomeRouteState) {
return _buildMenuIcon(true,context,routeBloc,"null",null);
} else if(state is LoginRouteState){
return _buildMenuIcon(false,context,routeBloc,"home",GoHomeEvent());
}
},
),
backgroundColorAppBar: Colors.transparent,
backgroundMenu: DecorationImage(
fit: BoxFit.cover,
image: AssetImage(NsiaAssets.drawerBackgroundPath),
),
elevationAppBar: 0.0,
),
PubScreen()
],
);
}
@override
void dispose() {
// TODO: implement dispose
//routeBloc.dispose();
drawerCtrl.dispose();
super.dispose();
}
}
here is the HomeScreen.dart class
import 'package:flutter/material.dart';
import '../routes/routes.dart';
import '../utils/app_assets.dart';
import '../widgets/radial_menu.dart';
import '../widgets/blinking_text.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../blocs/events/navigation_events.dart';
import '../blocs/navigation_bloc.dart';
import '../utils/screen_navigator.dart';
class HomeScreen extends StatefulWidget {
final Widget child;
HomeScreen({Key key, this.child}) : super(key: key);
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
Animation<double> fadeIn;
AnimationController controller;
NavigationBloc routeBloc;
void initState() {
super.initState();
controller = AnimationController(
duration: Duration(seconds: 1),
vsync: this,
);
fadeIn = Tween(begin: 0.0, end: 1.0)
.animate(CurvedAnimation(parent: controller, curve: Curves.easeIn));
controller.forward();
}
@override
void dispose() {
// TODO: implement dispose
routeBloc.dispose();
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
routeBloc=BlocProvider.of<NavigationBloc>(context);
double deviceHeight = MediaQuery.of(context).size.height;
debugPrint('hauteur du device : ${MediaQuery.of(context).size.height}');
double bar = AppBar().preferredSize.height;
return FadeTransition(
opacity: fadeIn,
child: Column(
children: <Widget>[
SizedBox(
height: bar,
),
Expanded(
child: Column(
children: <Widget>[
Container(
width: 300.0,
child: NsiaAssets.logoChapChapW,
),
SizedBox(height: deviceHeight <= 640.0 ? 15.0 : 30.0),
BlinkingText(
text:
"Bienvenue dans notre agence num茅rique mobile.\nAppuyez sur notre sigle pour commencer",
textColor: NsiaAssets.bleu,
alignment: TextAlign.center,
fontSize: 15.0,
),
SizedBox(height: deviceHeight <= 640.0 ? 100 : 140.0),
Container(
child: buildRadialMenu(context,routeBloc),
),
],
)),
Container(
height: 25,
child: Center(
child: Text(
"Tel: 22 41 98 00 | [email protected]",
style: TextStyle(color: Color(0xFF001093)),
),
),
)
],
),
);
}
Widget buildRadialMenu(BuildContext context,NavigationBloc bloc) {
return RadialMenu(
animationDuration: 800,
withRotation: false,
typeEntranceAnimation: TypeEntranceAnimation.slideInUp,
openIcon: Container(
width: 30.0,
height: 30.0,
child: NsiaAssets.sigleW,
),
menus: <RadialMenuItem>[
RadialMenuItem(
icon: Icon(Icons.person),
backgroundColor: NsiaAssets.bleu,
tooltip: "Nsia Chap Chap",
onPressed: () {
ScreenNavigator.goTo(context, "login", bloc,GoLoginEvent());
},
),
RadialMenuItem(
icon: Icon(Icons.supervisor_account),
backgroundColor: NsiaAssets.bleu,
tooltip: "Contactez nous",
onPressed: () {
debugPrint("here");
},
),
RadialMenuItem(
icon: Icon(Icons.settings),
backgroundColor: NsiaAssets.bleu,
tooltip: "R茅glages",
onPressed: () {
debugPrint("here");
},
),
RadialMenuItem(
icon: Icon(Icons.room),
backgroundColor: NsiaAssets.bleu,
tooltip: "Les agences Nsia",
onPressed: () {
debugPrint("here");
},
),
RadialMenuItem(
icon: Icon(Icons.shopping_basket),
backgroundColor: NsiaAssets.bleu,
tooltip: "notre catalogue de produits",
onPressed: () {
debugPrint("here");
},
),
RadialMenuItem(
icon: Icon(Icons.flash_on),
backgroundColor: NsiaAssets.bleu,
tooltip: "Restez informer avec Nsia vie news",
onPressed: () {
debugPrint("here");
},
),
],
);
}
}
I am able as you will see in the code above to dispatch an event to the bloc and go to the loginScreen, the transition shows in the console and the menu icon is changed to a back one, unfortunately for me when going from the LoginScreen back to the HomeScreen, the event is not dispatched, the switching of screens happen as intended but the bloc isnt notified of the screen change.
Also i noticed that if i navigate via the drawer i no longer have access to the bloc as the following gif shows.
What do i do wrong ? Am i missing something ?
@zjjt thanks the positive feedback and for opening an issue!
Would it be possible for you to share a link to the repo so I can just run the app locally? It would make it much easier to debug.
Hi @felangel , thanks for answering so quick i fell asleep during my debugging.
Here's the link to the repo.Ill have many bloc talking to each other so i might maybe need help for that later.Thanks a lot
App repo:
https://github.com/zjjt/chapchap.git
you will also need to have this package built and insert it in the pubsec.yaml. on my computer it is a local package so refer to it via relative path
hidden_drawer_menu
https://github.com/zjjt/hidden_drawer_menu.git
Hi @felangel , after tweaking the code a bit, i was able to make it work by removing bloc.dispose in my widgets...Aren't we suppose to always dispose of the bloc to avoid memory leak ? Or should i only dispose of the bloc in the last widget that makes use of it ?
@zjjt you are supposed to dispose but you need to make sure you dispose only when you no longer need the bloc. My general recommendation is to dispose the bloc in the same widget where it is created. Does that make sense?
@felangel m Yes it does make sense, so i shouldnt pass the bloc around to other widgets / methods. If i need the bloc i can use BlocProvider.of method...Got it ill close this issue then.Thanks a lot for your time
@zjjt awesome!
Yeah I'd recommend using BlocProvider when possible 馃憤
Glad I was able to help and thanks for bringing this up! 馃挴
Hi @felangel
so, i have the same problem when i dispatch an event a second time ... nothing happens.
BlocProvider.of<ReviewsBloc>(context).dispatch(AddReview());
the first time is
BlocProvider<ReviewsBloc>(
builder: (context) => ReviewsBloc(httpClient:http.Client())..dispatch(FetchReviews())
thank you
@oussemaMetoui that most likely means that the bloc thinks the state you yielded the second time is the same as the bloc's current state. Are your bloc states extending Equatable
? If yes, are you making sure to pass all of the class props to super
?
@felangel yes
i forgot to mention that BlocProvider.of<ReviewsBloc>(context).dispatch(AddReview());
doesn't call the method mapEventToState :confused:
`@immutable
abstract class ReviewsState extends Equatable {
ReviewsState([List props = const []]) : super(props);
}`
ReviewsEvent
@immutable
abstract class ReviewsEvent extends Equatable {
ReviewsEvent([List props = const <dynamic>[]]) : super(props);
}
//...
class AddReview extends ReviewsEvent {
final String gymId;
final int value;
AddReview({@required this.gymId, @required this.value})
: super([gymId, value]);
@override
String toString() => 'AddReview { gymId : $gymId, value : $value }';
}
ReviewsState
@immutable
abstract class ReviewsState extends Equatable {
ReviewsState([List props = const []]) : super(props);
}
//...
class ReviewAdded extends ReviewsState{
@override
String toString() => 'ReviewAdded';
}
class CardDetailGym extends StatelessWidget {
//...
onRatingUpdate: (rating)
BlocProvider.of<ReviewsBloc>(context)
.dispatch(AddReview(
value: rating.toInt(),
gymId: gym.sId));
},
hope so much you could help me
@oussemaMetoui can you please provide a link to a sample app which illustrates the issue you're having? It would be much easier for me to help if I can run the code locally.
@felangel
so this is the repo
got same issue @felangel , dispatch event once success and the second data didn't appear.
here the code
class LeagueBloc extends Bloc<LeagueEvent, LeagueState>{
final GenerateData generateData;
LeagueBloc({@required this.generateData});
@override
LeagueState get initialState => DefaultLeague(generateData.generateLeagueMatch(League.PREMIER));
@override
Stream<LeagueState> mapEventToState(LeagueEvent event) {
print("league 11 "+event.toString());
if(event is ChangeLeague){
return mapLeagueToState(event);
}else{
return mapLeagueToState(ChangeLeague(League.PREMIER));
}
}
Stream<LeagueState> mapLeagueToState(ChangeLeague changeLeague) async* {
try {
final matches = this.generateData.generateLeagueMatch(changeLeague.league);
yield LeagueLoaded(matches);
print("league 22 "+matches.toString());
} catch (e) {
print("error "+e.toString());
}
}
}
@immutable
class LeagueEvent extends Equatable{
LeagueEvent([List props = const []]) : super(props);
}
class ChangeLeague extends LeagueEvent{
final League league;
ChangeLeague(this.league) : super([league]);
}
@immutable
class LeagueState extends Equatable{
LeagueState([List props = const <dynamic>[]]) : super(props);
}
class LeagueLoaded extends LeagueState{
final List<Match> listMatch;
LeagueLoaded([this.listMatch = const[]]) : super(listMatch);
@override
String toString() => "League Loaded $listMatch.toString()";
}
class DefaultLeague extends LeagueState{
final League league = League.PREMIER;
final List<Match> listMatch;
DefaultLeague([this.listMatch = const[]]) : super(listMatch);
}
The View
BlocBuilder<LeagueBloc, LeagueState>(
if(state is LeagueLoaded){
print("league 33 "+state.toString());
}
bloc: leagueBloc,
builder: (context, state){
child: ListView.builder(
shrinkWrap: false,
itemBuilder: (context, pos){
final match = state.props[pos];
return ItemMatch(match: match);
},
itemCount: state.props.length,
)
}
}
Step: Click and 1st dispatch changeLeague
data has success changed. And than 2nd dispatcher event league 33
text didn't triggered
@oussemaMetoui the issue with your repo is that you have:
yield ReviewAdded();
so after the state is ReviewAdded()
if you yield ReviewAdded()
again the bloc will ignore the state because currentState == ReviewAdded()
will evaluate to true.
In order to resolve this, you either need to make sure to uniquely identify the ReviewAdded
states by passing a Review
to the state like:
class ReviewAdded extends ReviewsState {
final Review review;
ReviewAdded(this.review) : super([review]);
@override
String toString() => 'ReviewAdded';
}
@ariefannur I'm guessing the reason why you're having this issue is because either Match
or League
do not extend Equatable
.
still get same error @felangel here my model
class Match extends Equatable{
final Team teamA;
final Team teamB;
final String stadium;
final String date;
final String time;
final League league;
Match({this.teamA, this.teamB, this.stadium, this.date, this.time, this.league}) : super([teamA, teamB, stadium, date, time, league]);
@override
String toString() {
return "$teamA.toString() , $teamB.toString(), $stadium, $date, $time, $league.toString()";
}
@override
List<Object> get props {
return [teamA, teamB, stadium, date, time, league];
}
}
enum League {
PREMIER,
LALIGA,
SERIEA,
CHAMPIONS,
EROPA_LEAGUE
}
@ariefannur does team also extend Equatable ?
solve it's my mistake, forgot call super Equatable in model. Thanks @felangel @bigword12
@ariefannur I'm facing same problem, can you share your fixed models?
@netfirms usually you aren't passing the props to the super class if you're extending Equatable or in mapEventToState when you yield you are yielding a modified version of a previous state instead of creating a new instance. Hope that helps 馃憤
@felangel very helpful, I'm able to solve this problem right now.
I'm facing the same issue. The state isn't received the second time it's dispatched. This is my State
class RestartedAppState extends ConfigState{
RestartedAppState():super([]);
}
and ConfigState
extends Equatable
@immutable
abstract class ConfigState extends Equatable {
ConfigState([List props = const <dynamic>[]]) : super(props);
}
Not sure what I might be doing wrong? @felangel Can you help?
@adityadroid : check your bloc class is implemented singleton or not
I use template that auto create singleton then it causes error
@adityadroid : check your bloc class is implemented singleton or not
I use template that auto create singleton then it causes error
No. Its not a singleton. I create one instance in main.dart and use BlocProvider to inject it wherever I Need it.
can you provide a link to a sample app which illustrates the issue you're having? It would be much easier for me to help if I can run the code locally.
can you provide a link to a sample app which illustrates the issue you're having? It would be much easier for me to help if I can run the code locally.
This is the app. The bloc is defined in lib/blocs/config
. I'm trying to read the state in main.dart
can you provide a link to a sample app which illustrates the issue you're having? It would be much easier for me to help if I can run the code locally.
@nguyenhuutinh Here's a minimal reproduction of the issue
Steps to reproduce:
@adityadroid the issue is you're extending Equatable
for your ConfigState
and after you logout the first time, the bloc's state is RestartedAppState
. Then the second time you press logout internally bloc will do a check to see if the yielded state is different than the currentState
. In your case the new state is RestartedAppState()
and the currentState
is RestartedAppState()
so
nextState == currentState
would evaluate to true
and bloc would ignore the transition.
If you want the bloc to keep emitting the same state over and over then you should not extend Equatable
like:
import 'package:flutter/material.dart';
import 'package:meta/meta.dart';
@immutable
abstract class ConfigState {}
class ConfigChangeState extends ConfigState {
final String key;
final bool value;
ConfigChangeState(this.key, this.value);
}
class UnConfigState extends ConfigState {}
class UpdatingProfilePictureState extends ConfigState {}
class ProfilePictureChangedState extends ConfigState {
final String profilePictureUrl;
ProfilePictureChangedState(this.profilePictureUrl);
@override
String toString() =>
'ProfilePictureChangedState {profilePictureUrl: $profilePictureUrl}';
}
class RestartedAppState extends ConfigState {
RestartedAppState();
}
The real problem here though is you are changing the state when you logout but you are not changing the state when you login. If you also dispatched an event on login you wouldn't have this problem.
Hope that helps 馃憤
Most helpful comment
@felangel very helpful, I'm able to solve this problem right now.